Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src-node/ai-model-effort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/*
* Which thinking effort, if any, a query from the AI panel sends. The panel offers only the levels the
* SDK reports for the selected model; this checks the request again against the model list this
* process has, so a level the model does not support is never sent. No effort at all is the default:
* the SDK then applies the user's saved setting or the model's own default.
*/

const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];

/**
* The effort to set on a query's options, or undefined to send none.
*
* Before the first query of this process the model list is not known yet; the panel has checked the
* level against the list it kept from an earlier run, so it is sent rather than silently dropped. A
* custom endpoint's models are not Anthropic's, so their capabilities are unknown and nothing is sent.
*
* @param {Object} request
* @param {string} [request.effort] - The level the panel asked for.
* @param {string} [request.model] - The model the query names; none for the default model.
* @param {Array<Object>} [request.models] - The SDK's supportedModels() list, when this process has it.
* @param {string} [request.resolvedDefaultModel] - The model the default resolved to last, when known.
* @param {boolean} [request.customEndpoint] - Whether the query goes to a custom endpoint.
* @return {string|undefined} A level the model supports, or undefined.
*/
function effortForQuery(request) {
const { effort, model, models, resolvedDefaultModel, customEndpoint } = request || {};
if (!EFFORT_LEVELS.includes(effort) || customEndpoint) {
return undefined;
}
const target = model || resolvedDefaultModel;
if (!Array.isArray(models) || !models.length || !target) {
return effort;
}
const entry = models.find(function (m) {
return m && (m.value === target || m.resolvedModel === target);
});
if (!entry || !entry.supportsEffort || !Array.isArray(entry.supportedEffortLevels)) {
return undefined;
}
return entry.supportedEffortLevels.includes(effort) ? effort : undefined;
}

exports.EFFORT_LEVELS = EFFORT_LEVELS;
exports.effortForQuery = effortForQuery;
35 changes: 31 additions & 4 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ImagePreview = require("./ai-image-preview");
const {buildSystemPrompt, buildEditorContextLine} = require("./ai-system-prompt");
const CliConnector = require("./ai-cli-connector");
const CliCapabilities = require("./ai-cli-capabilities");
const ModelEffort = require("./ai-model-effort");
const NodeConnector = require("./node-connector");

exports.readImagePreview = ImagePreview.readImage;
Expand Down Expand Up @@ -129,6 +130,12 @@ let currentSessionId = null;
// best-effort, from the first live query. null until available.
let cachedModelList = null;

// The model the last query sent without a model resolved to (the user's saved
// Claude Code default) and the endpoint it was resolved on, so an effort asked
// for on the default model is checked against that model, and only while the
// endpoint is the same. null until a default query has started.
let _lastResolvedDefault = null;

/**
* Fetch the model list from a live Query once per process. Best-effort:
* the control request can fail on older CLIs or non-streaming input —
Expand Down Expand Up @@ -812,13 +819,14 @@ function _isAiScratchPath(filePath) {

/**
* Send a prompt to Claude and stream results back to the browser.
* Called from browser via execPeer("sendPrompt", {prompt, projectPath, sessionAction, model}).
* Called from browser via execPeer("sendPrompt", {prompt, projectPath, sessionAction, model, effort}).
* effort is a thinking effort level for the model, or absent to leave the default to the SDK.
*
* Returns immediately with a requestId. Results are sent as events:
* aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
*/
exports.sendPrompt = async function (params) {
const { prompt, projectPath, sessionAction, model, locale, selectionContext, editorContext,
const { prompt, projectPath, sessionAction, model, effort, locale, selectionContext, editorContext,
images, envOverrides, permissionMode, additionalDirectories, aiScratchDir } = params;
if (typeof aiScratchDir === "string" && aiScratchDir) {
_aiScratchDir = aiScratchDir;
Expand Down Expand Up @@ -874,7 +882,7 @@ exports.sendPrompt = async function (params) {
}

// Run the query asynchronously — don't await here so we return requestId immediately
_runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode, additionalDirectories)
_runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode, additionalDirectories, effort)
.catch(err => {
console.error("[Phoenix AI] Query error:", err);
});
Expand Down Expand Up @@ -1063,7 +1071,7 @@ exports.clearClarification = async function () {
/**
* Internal: run a Claude SDK query and stream results back to the browser.
*/
async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode, additionalDirectories) {
async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode, additionalDirectories, effort) {
// Sync the runtime mutable that hooks read for permission decisions —
// setPermissionMode (peer) updates this same variable when the user
// cycles modes mid-stream.
Expand Down Expand Up @@ -2109,6 +2117,22 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
queryOptions.model = model;
}

// Thinking effort: only a level the model supports, and none at all for
// the default, so the user's saved setting or the model's own default applies.
// The endpoint is the query's own environment, inherited variables included.
const endpoint = queryOptions.env.ANTHROPIC_BASE_URL || "";
const effortLevel = ModelEffort.effortForQuery({
effort: effort,
model: model,
models: cachedModelList,
resolvedDefaultModel: _lastResolvedDefault && _lastResolvedDefault.endpoint === endpoint ?
_lastResolvedDefault.model : null,
customEndpoint: !!endpoint
});
if (effortLevel) {
queryOptions.effort = effortLevel;
}


// Resume session if we have an existing one (already cleared if sessionAction was "new")
if (currentSessionId) {
Expand Down Expand Up @@ -2260,6 +2284,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
// Claude Code default. requestedModel lets the browser tell
// an explicit pick apart from default resolution.
if (message.type === "system" && message.subtype === "init" && message.model) {
if (!model) {
_lastResolvedDefault = { model: message.model, endpoint: endpoint };
}
nodeConnector.triggerPeer("aiSessionInfo", {
model: message.model,
requestedModel: model || null
Expand Down
1 change: 1 addition & 0 deletions src-node/test-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ require("./test/test-ai-cli-connector");
require("./test/test-npm-node-shim");
require("./test/test-media-server");
require("./test/test-builder-hub");
require("./test/test-ai-model-effort");

const TEST_NODE_CONNECTOR_ID = "ph_test_connector";
const nodeConnector = NodeConnector.createNodeConnector(TEST_NODE_CONNECTOR_ID, exports);
Expand Down
35 changes: 35 additions & 0 deletions src-node/test/test-ai-model-effort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

// Test-only connector for the thinking effort a query sends; the check is pure, so it runs in place.
const NodeConnector = require("../node-connector");
const ModelEffort = require("../ai-model-effort");

/**
* @param {Object} request - As ai-model-effort's effortForQuery takes.
* @return {Promise<{effort: (string|null)}>} The level a query would send, or null for none.
*/
async function effortForQuery(request) {
const effort = ModelEffort.effortForQuery(request);
return { effort: effort === undefined ? null : effort };
}

exports.effortForQuery = effortForQuery;
NodeConnector.createNodeConnector("ph_test_ai_model_effort", exports);
25 changes: 25 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2694,6 +2694,15 @@ define({
"AI_CHAT_START_SETUP": "Set up",
"AI_CHAT_START_CHAT_RUNNING": "In progress",
"AI_CHAT_START_CLI_RUNNING": "Running",
"AI_CHAT_CLI_ADD_SESSION": "+ Add CLI session",
"AI_CHAT_CLI_RENAME_SESSION": "Rename session…",
"AI_CHAT_CLI_STOP_SESSION": "Stop session",
"AI_CHAT_CLI_REMOVE_SESSION": "Remove session",
"AI_CHAT_CLI_REMOVE_CONFIRM_TITLE": "Remove {0}?",
"AI_CHAT_CLI_REMOVE_CONFIRM_MSG": "This removes the session card and stops the running {0} session, including any task it is working on.",
"AI_CHAT_CLI_SESSION_OPTIONS": "Options for {0}",
"AI_CHAT_CLI_SESSION_NAME": "{0} {1}",
"AI_CHAT_CLI_SESSION_IDENTITY": "{0} · #{1}",
"AI_CHAT_START_CLI_FOLDED": "Use a CLI instead: {0}",
"AI_CHAT_START_CUSTOM_PROVIDER": "Custom endpoint or a provider like {0}? See {1}.",
"AI_CHAT_SURPRISE_ME_USER_MSG": "Surprise me!",
Expand Down Expand Up @@ -2992,6 +3001,22 @@ define({
"AI_CHAT_MODEL_SELECT_TITLE": "Choose the AI model for this chat",
"AI_CHAT_MODE_SELECT_TITLE": "Switch between the Claude Code chat and an embedded CLI terminal",
"AI_CHAT_MODEL_SWITCHED_NOTICE": "Switched to {0}. Applies from your next message; the first response may take a moment longer while the cache rebuilds.",
"AI_CHAT_EFFORT_TITLE": "Thinking effort",
"AI_CHAT_EFFORT_CURRENT": "Thinking effort: {0}",
"AI_CHAT_EFFORT_HEADING": "Thinking effort · {0}",
"AI_CHAT_EFFORT_DEFAULT": "Default",
"AI_CHAT_EFFORT_DEFAULT_LABEL": "Default effort",
"AI_CHAT_EFFORT_DEFAULT_HINT": "{0} decides how much to think",
"AI_CHAT_EFFORT_LOW": "Low",
"AI_CHAT_EFFORT_MEDIUM": "Medium",
"AI_CHAT_EFFORT_HIGH": "High",
"AI_CHAT_EFFORT_XHIGH": "Extra high",
"AI_CHAT_EFFORT_MAX": "Max",
"AI_CHAT_EFFORT_LOW_HINT": "Fastest, least thinking",
"AI_CHAT_EFFORT_MEDIUM_HINT": "Balanced speed and depth",
"AI_CHAT_EFFORT_HIGH_HINT": "Deep reasoning",
"AI_CHAT_EFFORT_XHIGH_HINT": "Deeper than high",
"AI_CHAT_EFFORT_MAX_HINT": "Most thorough, slowest",
"AI_CHAT_INPUT_HINT": "Press {0} to send · {1} for new line",
"AI_CHAT_BASH_CONFIRM_TITLE": "Allow command?",
"AI_CHAT_BASH_ALLOW": "Allow",
Expand Down
Loading
Loading