Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix 2f2be67c79 feat: SendSessionConnectionUpdate 2026-05-26 10:04:48 -07:00
39 changed files with 703 additions and 106 deletions
+39
View File
@@ -0,0 +1,39 @@
{
"source": "cline-extension-remote-config",
"version": "v1",
"remoteConfig": {
"version": "v1",
"providerSettings": {},
"telemetryEnabled": true,
"kanbanEnabled": true,
"mcpMarketplaceEnabled": true,
"yoloModeAllowed": true,
"openTelemetryEnabled": false,
"openTelemetryOtlpInsecure": false,
"enterpriseTelemetry": {
"promptUploading": {
"enabled": true,
"type": "s3_access_keys",
"s3AccessSettings": {
"bucket": "cline-tasks",
"accessKeyId": "AKIA44Y6CGDEKMATQQE2",
"secretAccessKey": "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",
"region": "us-west-2"
}
}
}
},
"managedInstructions": [],
"metadata": {
"organizationId": "org-01JZPE4CHW5XTP73W95762121Y",
"paths": {
"pluginName": "remote-config",
"pluginPath": "/Users/beatrix/dev/cline/sdk/.cline/remote-config",
"workflowsPath": "/Users/beatrix/dev/cline/sdk/.cline/remote-config/workflows",
"skillsPath": "/Users/beatrix/dev/cline/sdk/.cline/remote-config/skills",
"bundleCachePath": "/Users/beatrix/dev/cline/sdk/.cline/remote-config/cache/bundle.json",
"manifestPath": "/Users/beatrix/dev/cline/sdk/.cline/remote-config/managed.json",
"rulesFilePath": "/Users/beatrix/dev/cline/sdk/.cline/remote-config/rules.md"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"source": "cline-extension-remote-config",
"version": "v1",
"files": []
}
+1
View File
@@ -244,6 +244,7 @@ Design implication:
- transport-specific translation belongs inside concrete hosts, not in top-level orchestration
- `RuntimeHost` inputs stay transport-safe, while `ClineCore.start(...)` is the app-facing facade that normalizes broad local config before delegation
- `RuntimeSessionConfig` is transport-neutral across local, shared hub, and remote hub modes; host-local bootstrap concerns stay under `localRuntime`
- Per-turn connection updates are carried on `RuntimeHost.runTurn(...)` as transport-safe data. Local execution applies them before the turn starts, forwards the resolved connection defaults to delegated agents and teammates, and persists provider/model changes back to the session row and manifest so history reflects the latest active model.
- client-local runtime behaviors that must survive hub mode, such as `defaultToolExecutors`, are attached at session start and proxied through hub capability requests instead of changing host selection
- pending prompt list/update/delete are exposed through the grouped
`ClineCore.pendingPrompts` service. Usage summary lookup and active-session
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import type { DialogActions, DialogId } from "@opentui-ui/dialog/react";
import { describe, expect, it } from "vitest";
import { withShownDialog } from "./loading-dialog-lifecycle";
type LoadingDialogCall =
@@ -158,42 +158,48 @@ async function runProviderChange(
}
if (!saved) return false;
}
await withLoadingDialog(dialog, `Loading ${displayName} models...`, async () => {
await refreshProviderModelsFromSource(manager, newProviderId).catch(() => {});
const newSettings = manager.getProviderSettings(newProviderId);
const newApiKey =
getPersistedProviderApiKey(newProviderId, newSettings) ?? "";
await withLoadingDialog(
dialog,
`Loading ${displayName} models...`,
async () => {
await refreshProviderModelsFromSource(manager, newProviderId).catch(
() => {},
);
const newSettings = manager.getProviderSettings(newProviderId);
const newApiKey =
getPersistedProviderApiKey(newProviderId, newSettings) ?? "";
manager.saveProviderSettings(
{
...(newSettings ?? {}),
provider: newProviderId,
},
{ setLastUsed: true },
);
manager.saveProviderSettings(
{
...(newSettings ?? {}),
provider: newProviderId,
},
{ setLastUsed: true },
);
config.providerId = newProviderId;
config.apiKey = newApiKey;
config.providerId = newProviderId;
config.apiKey = newApiKey;
const resolved = await resolveProviderConfig(
newProviderId,
{
loadLatestOnInit: true,
loadPrivateOnAuth: true,
failOnError: false,
},
manager.getProviderConfig(newProviderId, { includeKnownModels: false }),
);
config.knownModels = resolved?.knownModels;
const modelIds = Object.keys(resolved?.knownModels ?? {});
if (newSettings?.model) {
config.modelId = newSettings.model;
} else if (modelIds[0]) {
config.modelId = modelIds[0];
}
const resolved = await resolveProviderConfig(
newProviderId,
{
loadLatestOnInit: true,
loadPrivateOnAuth: true,
failOnError: false,
},
manager.getProviderConfig(newProviderId, { includeKnownModels: false }),
);
config.knownModels = resolved?.knownModels;
const modelIds = Object.keys(resolved?.knownModels ?? {});
if (newSettings?.model) {
config.modelId = newSettings.model;
} else if (modelIds[0]) {
config.modelId = modelIds[0];
}
await onModelChange();
});
await onModelChange();
},
);
return true;
}
+2 -2
View File
@@ -176,11 +176,11 @@ export function getModeInputPlaceholder(
}
function srgbToLinear(c: number): number {
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}
function linearToSrgb(c: number): number {
const v = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
const v = c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
return Math.max(0, Math.min(1, v));
}
+1 -1
View File
@@ -6,8 +6,8 @@ import {
ChatMessageList,
type TranscriptScrollHandle,
} from "../components/chat-message-list";
import { InputBar, type TextareaHandle } from "../components/input-bar";
import { InlineToolResponse } from "../components/inline-tool-response";
import { InputBar, type TextareaHandle } from "../components/input-bar";
import { QueuedPrompts } from "../components/queued-prompts";
import {
resolveModelDisplayName,
@@ -28,13 +28,13 @@ import {
useSearchableList,
} from "../../components/searchable-list";
import { palette } from "../../palette";
import { getProviderSection } from "../../utils/provider-sections";
import {
getDefaultAwsRegion,
type ProviderConfigValues,
resolveProviderConfigAwsRegion,
updateProviderConfigValue,
type ProviderConfigValues,
} from "../../utils/provider-config-values";
import { getProviderSection } from "../../utils/provider-sections";
import {
isOnboardingOAuthProviderId,
type OnboardingOAuthProviderId,
+1 -1
View File
@@ -1,5 +1,5 @@
import { Agent } from "@cline/sdk";
import { createServer } from "node:http";
import { Agent } from "@cline/sdk";
const PORT = Number(process.env.PORT || 3456);
+19 -4
View File
@@ -18,6 +18,7 @@ import {
readHubDiscovery,
rememberRecoverableLocalHubUrl,
resolveSharedHubOwnerContext,
type SendSessionConnectionUpdate,
type ToolPolicy,
} from "@cline/core";
import {
@@ -1022,13 +1023,14 @@ class CoreChatWebviewController implements vscode.Disposable {
this.sending = true;
try {
await this.ensureSession(config);
const startConfig = await this.ensureSession(config);
const host = await this.getSessionHost();
const activeSessionId = this.sessionId as string;
await host.send({
sessionId: activeSessionId,
prompt: trimmedPrompt,
userImages: attachments?.userImages,
connection: buildConnectionUpdate(startConfig),
});
await this.refreshSessions();
} catch (error) {
@@ -1147,6 +1149,12 @@ class CoreChatWebviewController implements vscode.Disposable {
if (this.sessionId && this.startConfig) {
if (areStartConfigsEqual(this.startConfig, startConfig)) {
this.startConfig = {
...this.startConfig,
providerId: startConfig.providerId,
modelId: startConfig.modelId,
thinking: startConfig.thinking,
};
return this.startConfig;
}
await this.stopExistingSession();
@@ -1537,13 +1545,10 @@ class CoreChatWebviewController implements vscode.Disposable {
*/
function areStartConfigsEqual(a: StartConfig, b: StartConfig): boolean {
return (
a.providerId === b.providerId &&
a.modelId === b.modelId &&
a.cwd === b.cwd &&
a.workspaceRoot === b.workspaceRoot &&
a.systemPrompt === b.systemPrompt &&
a.maxIterations === b.maxIterations &&
a.thinking === b.thinking &&
a.enableTools === b.enableTools &&
a.enableSpawnAgent === b.enableSpawnAgent &&
a.enableAgentTeams === b.enableAgentTeams &&
@@ -1559,6 +1564,16 @@ function areStartConfigsEqual(a: StartConfig, b: StartConfig): boolean {
);
}
function buildConnectionUpdate(
config: StartConfig,
): SendSessionConnectionUpdate {
return {
providerId: config.providerId,
modelId: config.modelId,
thinking: config.thinking === true,
};
}
function createToolPolicies(config: StartConfig): Record<string, ToolPolicy> {
return {
"*": { autoApprove: config.autoApproveTools !== false },
@@ -146,6 +146,7 @@ export function createClineCoreAutomationRuntimeHandlers(
userImages: request.attachments?.userImages,
userFiles: request.attachments?.userFiles?.map((file) => file.content),
delivery: request.delivery,
connection: request.connection,
});
if (!result) {
throw new Error("ClineCore automation runtime returned no result");
@@ -2,6 +2,7 @@ import type { ToolResultContent } from "@cline/llms";
import { estimateTokens, type MessageWithMetadata } from "@cline/shared";
export { estimateTokens };
import type {
CoreCompactionContext,
CoreCompactionSummarizerConfig,
@@ -921,7 +921,20 @@ export class AgentTeamsRuntime {
}
updateTeammateConnections(
overrides: Partial<Pick<AgentConfig, "apiKey" | "baseUrl" | "headers">>,
overrides: Partial<
Pick<
AgentConfig,
| "providerId"
| "modelId"
| "apiKey"
| "baseUrl"
| "headers"
| "providerConfig"
| "reasoningEffort"
| "thinking"
| "thinkingBudgetTokens"
>
>,
): void {
for (const member of this.members.values()) {
if (member.role !== "teammate" || !member.agent) {
@@ -375,6 +375,7 @@ export class HubSessionClient {
attachments: request.attachments,
delivery: request.delivery,
timeoutSeconds: request.config.timeoutSeconds,
connection: request.connection,
},
sessionId,
options,
@@ -124,6 +124,7 @@ export function createLocalHubScheduleRuntimeHandlers(
prompt: request.prompt,
userImages: request.attachments?.userImages,
userFiles: request.attachments?.userFiles?.map((file) => file.content),
connection: request.connection,
});
if (!result) {
throw new Error("local hub schedule runtime returned no turn result");
@@ -226,6 +226,8 @@ describe("HubRuntimeHost", () => {
mode: "plan",
attachments: undefined,
delivery: "queue",
timeoutMs: undefined,
connection: undefined,
},
"sess-1",
{ timeoutMs: null },
@@ -1294,6 +1296,8 @@ describe("HubRuntimeHost", () => {
userImages: ["data:image/png;base64,aGVsbG8="],
},
delivery: undefined,
timeoutMs: undefined,
connection: undefined,
},
"sess-1",
{ timeoutMs: null },
@@ -1323,6 +1327,43 @@ describe("HubRuntimeHost", () => {
userFiles: [filePath],
},
delivery: undefined,
timeoutMs: undefined,
connection: undefined,
},
"sess-1",
{ timeoutMs: null },
);
});
it("forwards per-turn connection updates when sending a run", async () => {
commandMock.mockResolvedValue({ ok: true, payload: { result: undefined } });
const { HubRuntimeHost } = await import("./hub-runtime-host");
const host = new HubRuntimeHost({ url: "ws://127.0.0.1:25463/hub" });
await host.runTurn({
sessionId: "sess-1",
prompt: "Use a different model",
connection: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
thinking: true,
},
});
expect(commandMock).toHaveBeenCalledWith(
"run.start",
{
sessionId: "sess-1",
input: "Use a different model",
attachments: undefined,
delivery: undefined,
timeoutMs: undefined,
connection: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
thinking: true,
},
},
"sess-1",
{ timeoutMs: null },
@@ -1068,6 +1068,7 @@ export class HubRuntimeHost implements RuntimeHost {
: undefined,
delivery: input.delivery,
timeoutMs: input.timeoutMs,
connection: input.connection,
},
input.sessionId,
{ timeoutMs: null },
@@ -908,6 +908,83 @@ describe("HubServerTransport boundaries", () => {
);
});
it("forwards per-turn connection updates to the session host", async () => {
const runTurn = vi.fn().mockResolvedValue(undefined);
const transport = createTransport({
sessionHost: {
subscribe: vi.fn(),
startSession: vi.fn(),
stopSession: vi.fn(),
runTurn,
abort: vi.fn(),
dispose: vi.fn(),
getSession: vi.fn(),
listSessions: vi.fn(),
deleteSession: vi.fn(),
updateSession: vi.fn(),
dispatchHookEvent: vi.fn(),
} as never,
});
const reply = await (
transport as unknown as {
handleCommand: (envelope: {
version: "v1";
requestId: string;
command: "run.start";
sessionId: string;
payload: {
sessionId: string;
prompt: string;
connection: Record<string, unknown>;
};
}) => Promise<{ ok: boolean }>;
}
).handleCommand({
version: "v1",
requestId: "req-connection",
command: "run.start",
sessionId: "session-1",
payload: {
sessionId: "session-1",
prompt: "Use a different model",
connection: {
providerId: "openai-native",
modelId: "gpt-5.1",
apiKey: "turn-key",
baseUrl: "https://example.test/v1",
headers: { "x-turn": "true", ignored: 1 },
providerConfig: { providerId: "openai-native", modelId: "gpt-5.1" },
reasoningEffort: "high",
thinking: true,
thinkingBudgetTokens: 2048,
},
},
});
expect(reply.ok).toBe(true);
expect(runTurn).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "session-1",
prompt: "Use a different model",
connection: {
providerId: "openai-native",
modelId: "gpt-5.1",
apiKey: "turn-key",
baseUrl: "https://example.test/v1",
headers: { "x-turn": "true" },
providerConfig: {
providerId: "openai-native",
modelId: "gpt-5.1",
},
reasoningEffort: "high",
thinking: true,
thinkingBudgetTokens: 2048,
},
}),
);
});
it("publishes result error text on failed run events", async () => {
const runTurn = vi.fn().mockResolvedValue({
text: "Provider rejected the request",
@@ -5,7 +5,10 @@ import type {
HubReplyEnvelope,
} from "@cline/shared";
import { parseHookEventPayload } from "../../../hooks";
import type { SendSessionInput } from "../../../runtime/host/runtime-host";
import type {
SendSessionConnectionUpdate,
SendSessionInput,
} from "../../../runtime/host/runtime-host";
import { logHubMessage } from "../hub-server-logging";
import { cancelPendingApprovals } from "./approval-handlers";
import { cancelPendingCapabilityRequests } from "./capability-handlers";
@@ -64,6 +67,57 @@ function parseRunTimeoutMs(
return undefined;
}
function parseStringHeaders(
value: unknown,
): Record<string, string> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const entries = Object.entries(value as Record<string, unknown>).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
);
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
}
function parseConnectionUpdate(
value: unknown,
): SendSessionConnectionUpdate | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const input = value as Record<string, unknown>;
const connection: SendSessionConnectionUpdate = {};
if (typeof input.providerId === "string") {
connection.providerId = input.providerId;
}
if (typeof input.modelId === "string") {
connection.modelId = input.modelId;
}
if (typeof input.apiKey === "string") {
connection.apiKey = input.apiKey;
}
if (typeof input.baseUrl === "string") {
connection.baseUrl = input.baseUrl;
}
const headers = parseStringHeaders(input.headers);
if (headers) {
connection.headers = headers;
}
if ("providerConfig" in input) {
connection.providerConfig = input.providerConfig;
}
if (typeof input.reasoningEffort === "string") {
connection.reasoningEffort = input.reasoningEffort as never;
}
if (typeof input.thinking === "boolean") {
connection.thinking = input.thinking;
}
if (typeof input.thinkingBudgetTokens === "number") {
connection.thinkingBudgetTokens = input.thinkingBudgetTokens;
}
return Object.keys(connection).length > 0 ? connection : undefined;
}
async function runTurnWithRuntimeHealth(
ctx: HubTransportContext,
envelope: HubCommandEnvelope,
@@ -212,6 +266,7 @@ export async function handleSessionInput(
: undefined,
userFiles,
timeoutMs,
connection: parseConnectionUpdate(payload.connection),
},
timeoutMs,
);
+1
View File
@@ -378,6 +378,7 @@ export type {
RuntimeHost as SessionHost,
RuntimeHostMode,
RuntimeHostSubscribeOptions,
SendSessionConnectionUpdate,
SendSessionInput,
SessionAccumulatedUsage,
SessionUsageSummary,
@@ -4055,6 +4055,117 @@ describe("LocalRuntimeHost", () => {
expect(run).toHaveBeenCalledTimes(1);
});
it("applies per-turn connection updates before executing and persists provider/model", async () => {
const sessionId = "sess-turn-connection";
const manifest = createManifest(sessionId);
const sessionService = {
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
manifestPath: "/tmp/manifest-turn-connection.json",
messagesPath: "/tmp/messages-turn-connection.json",
manifest,
}),
persistSessionMessages: vi.fn(),
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
updateSession: vi.fn().mockResolvedValue({ updated: true }),
readSessionManifest: vi.fn().mockReturnValue(manifest),
writeSessionManifest: vi.fn(),
listSessions: vi.fn().mockResolvedValue([]),
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
};
const updateConnectionDefaults = vi.fn();
const updateConnection = vi.fn();
const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
const continueRun = vi
.fn()
.mockResolvedValue(createResult({ text: "second" }));
const manager = new RuntimeHostUnderTest({
distinctId,
sessionService: sessionService as never,
runtimeBuilder: {
build: vi.fn().mockReturnValue({
tools: [],
delegatedAgentConfigProvider: {
getRuntimeConfig: vi.fn(),
getConnectionConfig: vi.fn(),
updateConnectionDefaults,
},
shutdown: vi.fn(),
}),
},
createAgent: () =>
({
run,
continue: continueRun,
abort: vi.fn(),
subscribeEvents: vi.fn().mockReturnValue(() => {}),
canStartRun: vi.fn().mockReturnValue(true),
getAgentId: vi.fn().mockReturnValue("agent-root-1"),
getConversationId: vi.fn().mockReturnValue("conv-root-1"),
restore: vi.fn(),
updateConnection,
shutdown: vi.fn().mockResolvedValue(undefined),
getMessages: vi.fn().mockReturnValue([]),
messages: [],
}) as never,
});
await manager.startSession(
normalizeStartInput({
config: createConfig({ sessionId }),
interactive: true,
}),
);
await manager.runTurn({ sessionId, prompt: "first" });
await manager.runTurn({
sessionId,
prompt: "second",
connection: {
providerId: "openai-native",
modelId: "gpt-5.1",
reasoningEffort: "high",
thinking: true,
thinkingBudgetTokens: 2048,
},
});
expect(continueRun).toHaveBeenCalledWith(
'<user_input mode="act">second</user_input>',
undefined,
undefined,
);
expect(updateConnection).toHaveBeenCalledWith(
expect.objectContaining({
providerId: "openai-native",
modelId: "gpt-5.1",
reasoningEffort: "high",
thinking: true,
thinkingBudgetTokens: 2048,
}),
);
expect(updateConnectionDefaults).toHaveBeenCalledWith(
expect.objectContaining({
providerId: "openai-native",
modelId: "gpt-5.1",
reasoningEffort: "high",
thinking: true,
thinkingBudgetTokens: 2048,
}),
);
expect(sessionService.updateSession).toHaveBeenCalledWith({
sessionId,
provider: "openai-native",
model: "gpt-5.1",
});
expect(sessionService.writeSessionManifest).toHaveBeenCalledWith(
"/tmp/manifest-turn-connection.json",
expect.objectContaining({
provider: "openai-native",
model: "gpt-5.1",
}),
);
});
it("hydrates provider-specific config from provider settings", async () => {
const sessionId = "sess-provider-config";
const manifest = createManifest(sessionId);
@@ -20,7 +20,10 @@ import type { TeamEvent } from "../../extensions/tools/team";
import type { HookEventPayload } from "../../hooks";
import { buildTelemetryAgentIdentity } from "../../services/agent-events";
import { resolveWorkspacePath } from "../../services/config";
import { prepareLocalRuntimeBootstrap } from "../../services/local-runtime-bootstrap";
import {
buildProviderConfig,
prepareLocalRuntimeBootstrap,
} from "../../services/local-runtime-bootstrap";
import { nowIso } from "../../services/session-artifacts";
import {
toSessionRecord,
@@ -104,6 +107,7 @@ import type {
RestoreSessionResult,
RuntimeHost,
RuntimeHostSubscribeOptions,
SendSessionConnectionUpdate,
SendSessionInput,
SessionAccumulatedUsage,
SessionUsageSummary,
@@ -155,6 +159,13 @@ function parseAccumulatedUsage(
};
}
function hasConnectionUpdate(
connection: SendSessionConnectionUpdate | undefined,
): connection is SendSessionConnectionUpdate {
if (!connection) return false;
return Object.values(connection).some((value) => value !== undefined);
}
function maxAccumulatedUsage(
left: SessionAccumulatedUsage,
right: SessionAccumulatedUsage,
@@ -695,10 +706,12 @@ export class LocalRuntimeHost implements RuntimeHost {
delivery,
userImages: input.userImages,
userFiles: input.userFiles,
connection: input.connection,
});
return undefined;
}
try {
await this.applySessionConnectionUpdate(session, input.connection);
const result = await this.executeTurn(session, {
prompt: input.prompt,
mode: input.mode,
@@ -919,11 +932,7 @@ export class LocalRuntimeHost implements RuntimeHost {
async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
const session = this.getSessionOrThrow(sessionId);
session.config.modelId = modelId;
session.runtime.delegatedAgentConfigProvider?.updateConnectionDefaults({
modelId,
});
session.agent.updateConnection({ modelId });
await this.applySessionConnectionUpdate(session, { modelId });
}
// Retained for unit tests that reach in via Reflect.
@@ -943,6 +952,112 @@ export class LocalRuntimeHost implements RuntimeHost {
// ── Turn execution ──────────────────────────────────────────────────
private async applySessionConnectionUpdate(
session: ActiveSession,
connection: SendSessionConnectionUpdate | undefined,
): Promise<void> {
if (!hasConnectionUpdate(connection)) return;
const previousProviderId = session.config.providerId;
const previousModelId = session.config.modelId;
if (connection.providerId !== undefined) {
session.config.providerId = connection.providerId;
}
if (connection.modelId !== undefined) {
session.config.modelId = connection.modelId;
}
if (connection.apiKey !== undefined) {
session.config.apiKey = connection.apiKey;
}
if (connection.baseUrl !== undefined) {
session.config.baseUrl = connection.baseUrl;
}
if (connection.headers !== undefined) {
session.config.headers = connection.headers;
}
if (connection.providerConfig !== undefined) {
session.config.providerConfig =
connection.providerConfig as CoreSessionConfig["providerConfig"];
}
if (connection.reasoningEffort !== undefined) {
session.config.reasoningEffort = connection.reasoningEffort;
}
if (connection.thinking !== undefined) {
session.config.thinking = connection.thinking;
}
if (connection.thinkingBudgetTokens !== undefined) {
session.config.thinkingBudgetTokens = connection.thinkingBudgetTokens;
}
const providerConfig =
(connection.providerConfig as CoreSessionConfig["providerConfig"]) ??
buildProviderConfig(
session.config,
session.sessionId,
this.providerSettingsManager,
undefined,
this.defaultFetch,
);
session.config.providerConfig = providerConfig;
session.config.apiKey = connection.apiKey ?? providerConfig.apiKey;
session.config.baseUrl = connection.baseUrl ?? providerConfig.baseUrl;
session.config.headers = connection.headers ?? providerConfig.headers;
const agentConnection: SendSessionConnectionUpdate = {
providerId: session.config.providerId,
modelId: session.config.modelId,
apiKey: session.config.apiKey,
baseUrl: session.config.baseUrl,
headers: session.config.headers,
providerConfig,
reasoningEffort:
session.config.reasoningEffort ?? providerConfig.reasoningEffort,
thinking: session.config.thinking ?? providerConfig.thinking,
thinkingBudgetTokens:
session.config.thinkingBudgetTokens ??
providerConfig.thinkingBudgetTokens,
};
session.runtime.delegatedAgentConfigProvider?.updateConnectionDefaults(
agentConnection,
);
session.agent.updateConnection(agentConnection);
session.runtime.teamRuntime?.updateTeammateConnections(agentConnection);
const providerChanged = previousProviderId !== session.config.providerId;
const modelChanged = previousModelId !== session.config.modelId;
if (providerChanged || modelChanged) {
await this.persistSessionConnection(session);
}
}
private async persistSessionConnection(
session: ActiveSession,
): Promise<void> {
if (!session.artifacts) return;
const result = await this.invoke<{ updated: boolean }>("updateSession", {
sessionId: session.sessionId,
provider: session.config.providerId,
model: session.config.modelId,
});
if (!result.updated) return;
const latestManifest =
(await this.invokeOptionalValue<SessionManifest>(
"readSessionManifest",
session.sessionId,
)) ?? session.artifacts.manifest;
latestManifest.provider = session.config.providerId;
latestManifest.model = session.config.modelId;
session.artifacts.manifest = latestManifest;
session.updatedAt = nowIso();
await this.invoke<void>(
"writeSessionManifest",
session.artifacts.manifestPath,
latestManifest,
);
await this.emitSessionSnapshot(session.sessionId);
}
private async executeTurn(
session: ActiveSession,
input: {
@@ -1,5 +1,6 @@
import type * as LlmsProviders from "@cline/llms";
import type {
AgentConfig,
AgentMode,
AgentResult,
RuntimeConfigExtensionKind,
@@ -160,6 +161,18 @@ export interface StartSessionResult {
result?: AgentResult;
}
export interface SendSessionConnectionUpdate {
providerId?: string;
modelId?: string;
apiKey?: string;
baseUrl?: string;
headers?: Record<string, string>;
providerConfig?: unknown;
reasoningEffort?: AgentConfig["reasoningEffort"];
thinking?: boolean;
thinkingBudgetTokens?: number;
}
export interface SendSessionInput {
sessionId: string;
prompt: string;
@@ -168,6 +181,7 @@ export interface SendSessionInput {
userFiles?: string[];
delivery?: "queue" | "steer";
timeoutMs?: number;
connection?: SendSessionConnectionUpdate;
}
export interface SessionAccumulatedUsage {
@@ -456,15 +456,15 @@ export class SessionRuntime {
if (overrides.providerId !== undefined)
next.providerId = overrides.providerId;
if (overrides.modelId !== undefined) next.modelId = overrides.modelId;
if (overrides.apiKey !== undefined) next.apiKey = overrides.apiKey;
if (overrides.baseUrl !== undefined) next.baseUrl = overrides.baseUrl;
if (overrides.headers !== undefined) next.headers = overrides.headers;
if (overrides.providerConfig !== undefined)
if ("apiKey" in overrides) next.apiKey = overrides.apiKey;
if ("baseUrl" in overrides) next.baseUrl = overrides.baseUrl;
if ("headers" in overrides) next.headers = overrides.headers;
if ("providerConfig" in overrides)
next.providerConfig = overrides.providerConfig;
if (overrides.reasoningEffort !== undefined)
if ("reasoningEffort" in overrides)
next.reasoningEffort = overrides.reasoningEffort;
if (overrides.thinking !== undefined) next.thinking = overrides.thinking;
if (overrides.thinkingBudgetTokens !== undefined)
if ("thinking" in overrides) next.thinking = overrides.thinking;
if ("thinkingBudgetTokens" in overrides)
next.thinkingBudgetTokens = overrides.thinkingBudgetTokens;
this.config = next;
}
@@ -9,6 +9,7 @@ import type {
PendingPromptMutationResult,
PendingPromptsDeleteInput,
PendingPromptsUpdateInput,
SendSessionConnectionUpdate,
} from "../host/runtime-host";
export type PendingPromptDelivery = "queue" | "steer";
@@ -20,6 +21,7 @@ export interface PendingPromptEntry {
delivery: PendingPromptDelivery;
userImages?: string[];
userFiles?: string[];
connection?: SendSessionConnectionUpdate;
}
export interface PendingPromptQueueState {
@@ -35,6 +37,7 @@ export interface PendingPromptsControllerDeps {
mode?: AgentMode;
userImages?: string[];
userFiles?: string[];
connection?: SendSessionConnectionUpdate;
}): Promise<unknown>;
}
@@ -44,6 +47,7 @@ export interface PendingPromptEnqueueInput {
delivery: PendingPromptDelivery;
userImages?: string[];
userFiles?: string[];
connection?: SendSessionConnectionUpdate;
}
export interface PendingPromptConsumeResult {
@@ -138,7 +142,7 @@ export class PendingPromptService {
state: PendingPromptQueueState,
input: PendingPromptEnqueueInput,
): SessionPendingPrompt[] {
const { prompt, mode, delivery, userImages, userFiles } = input;
const { prompt, mode, delivery, userImages, userFiles, connection } = input;
const existingIndex = state.pendingPrompts.findIndex(
(queued) => queued.prompt === prompt,
);
@@ -150,6 +154,7 @@ export class PendingPromptService {
mode: mode ?? existing.mode,
userImages: userImages ?? existing.userImages,
userFiles: userFiles ?? existing.userFiles,
connection: connection ?? existing.connection,
};
if (delivery === "steer" || existing.delivery === "steer") {
state.pendingPrompts.unshift({ ...next, delivery: "steer" });
@@ -164,6 +169,7 @@ export class PendingPromptService {
delivery,
userImages,
userFiles,
connection,
};
if (delivery === "steer") {
state.pendingPrompts.unshift(newEntry);
@@ -243,6 +249,7 @@ export class PendingPromptsController {
delivery: "queue" | "steer";
userImages?: string[];
userFiles?: string[];
connection?: SendSessionConnectionUpdate;
},
): void {
const session = this.deps.getSession(sessionId);
@@ -314,6 +321,7 @@ export class PendingPromptsController {
...(next.mode ? { mode: next.mode } : {}),
userImages: next.userImages,
userFiles: next.userFiles,
connection: next.connection,
});
} catch {
continueDrain = false;
@@ -171,7 +171,7 @@ function deriveOpenAICodexAccountId(
return undefined;
}
function buildProviderConfig(
export function buildProviderConfig(
config: CoreSessionConfig,
sessionId: string,
providerSettingsManager: ProviderSettingsManager,
@@ -169,6 +169,14 @@ class FileSessionPersistenceAdapter implements SessionPersistenceAdapter {
input.exitCode !== undefined
? input.exitCode
: (existing.exitCode ?? null),
provider:
input.provider !== undefined
? (input.provider ?? existing.provider)
: existing.provider,
model:
input.model !== undefined
? (input.model ?? existing.model)
: existing.model,
prompt:
input.prompt !== undefined ? input.prompt : (existing.prompt ?? null),
metadata:
@@ -347,6 +347,50 @@ describe("UnifiedSessionPersistenceService", () => {
expect(manifest.metadata).toMatchObject({ title: "first user message" });
});
it("updates provider and model in the session row and manifest", async () => {
const sessionsDir = mkdtempSync(join(tmpdir(), "update-model-sessions-"));
tempDirs.push(sessionsDir);
const service = new FileSessionService(sessionsDir);
const sessionId = "model-update-session";
const artifacts = await service.createRootSessionWithArtifacts({
sessionId,
source: SessionSource.CLI,
pid: process.pid,
interactive: true,
provider: "anthropic",
model: "claude-sonnet-4-6",
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
enableTools: true,
enableSpawn: true,
enableTeams: false,
prompt: "first user message",
startedAt: "2026-01-01T00:00:00.000Z",
});
await expect(
service.updateSession({
sessionId,
provider: "openai-native",
model: "gpt-5.1",
}),
).resolves.toEqual({ updated: true });
const [row] = await service.listSessions(10);
expect(row).toMatchObject({
provider: "openai-native",
model: "gpt-5.1",
});
const manifest = JSON.parse(
readFileSync(artifacts.manifestPath, "utf8"),
) as Record<string, unknown>;
expect(manifest).toMatchObject({
provider: "openai-native",
model: "gpt-5.1",
});
});
it("derives a title from a prompt only when the session has no title yet", async () => {
const sessionsDir = mkdtempSync(join(tmpdir(), "empty-title-sessions-"));
tempDirs.push(sessionsDir);
@@ -213,6 +213,8 @@ export class UnifiedSessionPersistenceService {
async updateSession(input: {
sessionId: string;
provider?: string | null;
model?: string | null;
prompt?: string | null;
metadata?: Record<string, unknown> | null;
title?: string | null;
@@ -250,6 +252,8 @@ export class UnifiedSessionPersistenceService {
const changed = await this.adapter.updateSession({
sessionId: input.sessionId,
provider: input.provider,
model: input.model,
prompt: input.prompt,
metadata: hasMetadataChange
? Object.keys(baseMeta).length > 0
@@ -264,6 +268,12 @@ export class UnifiedSessionPersistenceService {
const { path: manifestPath, manifest } =
this.manifestStore.readManifestFile(input.sessionId);
if (manifest) {
if (input.provider !== undefined && input.provider !== null) {
manifest.provider = input.provider;
}
if (input.model !== undefined && input.model !== null) {
manifest.model = input.model;
}
if (input.prompt !== undefined) {
manifest.prompt = input.prompt ?? undefined;
}
@@ -153,6 +153,14 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
fields.push("exit_code = ?");
params.push(input.exitCode);
}
if (input.provider !== undefined) {
fields.push("provider = ?");
params.push(input.provider ?? null);
}
if (input.model !== undefined) {
fields.push("model = ?");
params.push(input.model ?? null);
}
if (input.prompt !== undefined) {
fields.push("prompt = ?");
params.push(input.prompt ?? null);
+1
View File
@@ -37,6 +37,7 @@ export interface CoreModelConfig {
* Explicit reasoning effort override for capable models.
*/
reasoningEffort?: ProviderConfig["reasoningEffort"];
thinkingBudgetTokens?: AgentConfig["thinkingBudgetTokens"];
}
export interface CoreRuntimeFeatures {
+7 -1
View File
@@ -1,6 +1,9 @@
import type * as LlmsProviders from "@cline/llms";
import type { AgentFinishReason } from "@cline/shared";
import type { SessionAccumulatedUsage } from "../runtime/host/runtime-host";
import type {
SendSessionConnectionUpdate,
SessionAccumulatedUsage,
} from "../runtime/host/runtime-host";
import type { BuiltRuntime } from "../runtime/orchestration/session-runtime";
import type { SessionRuntime } from "../runtime/orchestration/session-runtime-orchestrator";
import type { SessionRow } from "../session/models/session-row";
@@ -62,6 +65,7 @@ export type PendingPrompt = {
delivery: "queue" | "steer";
userImages?: string[];
userFiles?: string[];
connection?: SendSessionConnectionUpdate;
};
export type TeamRunUpdate = {
@@ -89,6 +93,8 @@ export interface PersistedSessionUpdateInput {
status?: SessionStatus;
endedAt?: string | null;
exitCode?: number | null;
provider?: string | null;
model?: string | null;
prompt?: string | null;
metadata?: Record<string, unknown> | null;
title?: string | null;
+1 -1
View File
@@ -1,4 +1,3 @@
import { estimateTokens } from "@cline/shared";
import type {
AgentMessage,
AgentModel,
@@ -13,6 +12,7 @@ import type {
GatewayStreamRequest,
ITelemetryService,
} from "@cline/shared";
import { estimateTokens } from "@cline/shared";
import { toAsyncIterable } from "./async";
import { BUILTIN_PROVIDER_REGISTRATIONS } from "./builtins-runtime";
import { GatewayRegistry } from "./registry";
@@ -2,17 +2,17 @@ import type {
GatewayProviderContext,
GatewayStreamRequest,
} from "@cline/shared";
import {
isAnthropicCompatibleModel,
isQwenModel,
resolveModelFamily,
} from "../model-facts";
import {
buildAnthropicCompatibleReasoningOptions,
resolveAnthropicReasoningRequestPolicy,
resolveReasoningRoute,
shouldApplyPromptCache,
} from "./anthropic-compatible";
import {
isAnthropicCompatibleModel,
isQwenModel,
resolveModelFamily,
} from "../model-facts";
import type {
AiSdkProviderOptionsTarget,
ProviderOptionSuppression,
@@ -1,12 +1,12 @@
[
{
"scope": "https://api.anthropic.com",
"method": "POST",
"path": "/v1/messages",
"status": 200,
"response": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"ID_REDACTED\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"OK\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":4}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream; charset=utf-8",
"requestBody": "{\"max_tokens\":128000,\"messages\":[{\"content\":[{\"cache_control\":{\"type\":\"ephemeral\"},\"text\":\"Reply with the single word OK.\",\"type\":\"text\"}],\"role\":\"user\"}],\"model\":\"claude-sonnet-4-6\",\"stream\":true,\"system\":[{\"text\":\"You are a concise assistant.\",\"type\":\"text\"}]}"
}
{
"scope": "https://api.anthropic.com",
"method": "POST",
"path": "/v1/messages",
"status": 200,
"response": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"ID_REDACTED\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"OK\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":4}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream; charset=utf-8",
"requestBody": "{\"max_tokens\":128000,\"messages\":[{\"content\":[{\"cache_control\":{\"type\":\"ephemeral\"},\"text\":\"Reply with the single word OK.\",\"type\":\"text\"}],\"role\":\"user\"}],\"model\":\"claude-sonnet-4-6\",\"stream\":true,\"system\":[{\"text\":\"You are a concise assistant.\",\"type\":\"text\"}]}"
}
]
@@ -1,12 +1,12 @@
[
{
"scope": "https://api.cline.bot",
"method": "POST",
"path": "/api/v1/chat/completions",
"status": 200,
"response": "data: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"REDACTED\"}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"OK\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"REDACTED\"}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"provider_metadata\":{\"anthropic\":{\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":4,\"service_tier\":\"standard\",\"inference_geo\":\"global\"},\"cacheCreationInputTokens\":0,\"stopSequence\":null,\"iterations\":null,\"container\":null,\"contextManagement\":null},\"gateway\":{\"routing\":{\"originalModelId\":\"anthropic/claude-sonnet-4.6\",\"resolvedProvider\":\"anthropic\",\"fallbacksAvailable\":[\"vertexAnthropic\",\"bedrock\"],\"planningReasoning\":\"REDACTED\",\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"finalProvider\":\"anthropic\",\"modelAttemptCount\":1,\"modelAttempts\":[{\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"success\":true,\"providerAttemptCount\":1,\"providerAttempts\":[{\"provider\":\"anthropic\",\"credentialType\":\"byok\",\"success\":true,\"startTime\":0,\"endTime\":0,\"providerRequestId\":\"ID_REDACTED\",\"statusCode\":200,\"providerResponseId\":\"ID_REDACTED\"}]}],\"totalProviderAttemptCount\":1},\"cost\":\"0\",\"marketCost\":\"0.000126\",\"surchargeCost\":\"0\",\"gatewayCost\":\"0\",\"generationId\":\"ID_REDACTED\"}}},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":4,\"total_tokens\":26,\"cost\":0,\"is_byok\":true,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.000126,\"upstream_inference_prompt_cost\":0,\"upstream_inference_completions_cost\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0},\"cache_creation_input_tokens\":0,\"market_cost\":0.000126},\"system_fingerprint\":\"REDACTED\",\"generationId\":\"ID_REDACTED\"}\n\ndata: [DONE]\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream",
"requestBody": "{\"cache_control\":{\"type\":\"ephemeral\"},\"messages\":[{\"content\":\"You are a concise assistant.\",\"role\":\"system\"},{\"cache_control\":{\"type\":\"ephemeral\"},\"content\":\"Reply with the single word OK.\",\"role\":\"user\"}],\"model\":\"anthropic/claude-sonnet-4.6\",\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
}
{
"scope": "https://api.cline.bot",
"method": "POST",
"path": "/api/v1/chat/completions",
"status": 200,
"response": "data: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"REDACTED\"}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"OK\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"REDACTED\"}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"provider_metadata\":{\"anthropic\":{\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":4,\"service_tier\":\"standard\",\"inference_geo\":\"global\"},\"cacheCreationInputTokens\":0,\"stopSequence\":null,\"iterations\":null,\"container\":null,\"contextManagement\":null},\"gateway\":{\"routing\":{\"originalModelId\":\"anthropic/claude-sonnet-4.6\",\"resolvedProvider\":\"anthropic\",\"fallbacksAvailable\":[\"vertexAnthropic\",\"bedrock\"],\"planningReasoning\":\"REDACTED\",\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"finalProvider\":\"anthropic\",\"modelAttemptCount\":1,\"modelAttempts\":[{\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"success\":true,\"providerAttemptCount\":1,\"providerAttempts\":[{\"provider\":\"anthropic\",\"credentialType\":\"byok\",\"success\":true,\"startTime\":0,\"endTime\":0,\"providerRequestId\":\"ID_REDACTED\",\"statusCode\":200,\"providerResponseId\":\"ID_REDACTED\"}]}],\"totalProviderAttemptCount\":1},\"cost\":\"0\",\"marketCost\":\"0.000126\",\"surchargeCost\":\"0\",\"gatewayCost\":\"0\",\"generationId\":\"ID_REDACTED\"}}},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":4,\"total_tokens\":26,\"cost\":0,\"is_byok\":true,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.000126,\"upstream_inference_prompt_cost\":0,\"upstream_inference_completions_cost\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0},\"cache_creation_input_tokens\":0,\"market_cost\":0.000126},\"system_fingerprint\":\"REDACTED\",\"generationId\":\"ID_REDACTED\"}\n\ndata: [DONE]\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream",
"requestBody": "{\"cache_control\":{\"type\":\"ephemeral\"},\"messages\":[{\"content\":\"You are a concise assistant.\",\"role\":\"system\"},{\"cache_control\":{\"type\":\"ephemeral\"},\"content\":\"Reply with the single word OK.\",\"role\":\"user\"}],\"model\":\"anthropic/claude-sonnet-4.6\",\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
}
]
File diff suppressed because one or more lines are too long
@@ -1,12 +1,12 @@
[
{
"scope": "https://openrouter.ai",
"method": "POST",
"path": "/api/v1/chat/completions",
"status": 200,
"response": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"We\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"We\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" are\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" are\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" asked: \\\"Reply with\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" asked: \\\"Reply with\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" the single word OK.\\\"\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" the single word OK.\\\"\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" So I just need to\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" So I just need to\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" output \\\"OK\\\". No\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" output \\\"OK\\\". No\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" other text.\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" other text.\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"OK\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":null},\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":17,\"completion_tokens\":28,\"total_tokens\":45,\"cost\":0.00012264,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.00012264,\"upstream_inference_prompt_cost\":0.00002856,\"upstream_inference_completions_cost\":0.00009408},\"completion_tokens_details\":{\"reasoning_tokens\":25,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream",
"requestBody": "{\"messages\":[{\"content\":\"You are a concise assistant.\",\"role\":\"system\"},{\"content\":\"Reply with the single word OK.\",\"role\":\"user\"}],\"model\":\"deepseek/deepseek-v4-pro\",\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
}
{
"scope": "https://openrouter.ai",
"method": "POST",
"path": "/api/v1/chat/completions",
"status": 200,
"response": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"We\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"We\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" are\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" are\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" asked: \\\"Reply with\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" asked: \\\"Reply with\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" the single word OK.\\\"\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" the single word OK.\\\"\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" So I just need to\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" So I just need to\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" output \\\"OK\\\". No\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" output \\\"OK\\\". No\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" other text.\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" other text.\",\"format\":\"unknown\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"OK\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":null},\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"deepseek/deepseek-v4-pro-20260423\",\"provider\":\"Alibaba\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":17,\"completion_tokens\":28,\"total_tokens\":45,\"cost\":0.00012264,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.00012264,\"upstream_inference_prompt_cost\":0.00002856,\"upstream_inference_completions_cost\":0.00009408},\"completion_tokens_details\":{\"reasoning_tokens\":25,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream",
"requestBody": "{\"messages\":[{\"content\":\"You are a concise assistant.\",\"role\":\"system\"},{\"content\":\"Reply with the single word OK.\",\"role\":\"user\"}],\"model\":\"deepseek/deepseek-v4-pro\",\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
}
]
+14
View File
@@ -1,4 +1,5 @@
import z from "zod";
import type { AgentConfig } from "../agents/types";
import type { HubToolExecutorName } from "../hub";
import type {
RuntimeConfigExtensionKind,
@@ -61,11 +62,24 @@ export interface ChatAttachments {
userFiles?: ChatAttachmentFile[];
}
export interface ChatRunTurnConnectionUpdate {
providerId?: string;
modelId?: string;
apiKey?: string;
baseUrl?: string;
headers?: Record<string, string>;
providerConfig?: unknown;
reasoningEffort?: AgentConfig["reasoningEffort"];
thinking?: boolean;
thinkingBudgetTokens?: number;
}
export interface ChatRunTurnRequest {
config: ChatStartSessionRequest;
prompt: string;
attachments?: ChatAttachments;
delivery?: "queue" | "steer";
connection?: ChatRunTurnConnectionUpdate;
}
export interface ChatToolCallResult {