@juspay/neurolink 11.11.4 → 11.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * OpenCode client configurator.
3
+ *
4
+ * Moved verbatim out of `proxy.ts` so that adding a CLI means adding a file
5
+ * here rather than editing a 5,000-line command module in seven places.
6
+ */
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ import { logger } from "../../lib/utils/logger.js";
10
+ function getOpenCodeConfigDir() {
11
+ // OpenCode resolves this with the unmodified `xdg-basedir` package —
12
+ // `XDG_CONFIG_HOME || ~/.config` — on every platform, macOS included. There
13
+ // is deliberately no darwin branch here: `~/Library/Application Support/
14
+ // opencode` is not a path OpenCode reads. (The similar-looking literal in
15
+ // OpenCode's binary is `systemManagedConfigDir()`, an MDM policy directory
16
+ // at the filesystem root with no $HOME prefix.)
17
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
18
+ }
19
+ function getOpenCodeConfigPath() {
20
+ return join(getOpenCodeConfigDir(), "opencode.json");
21
+ }
22
+ /**
23
+ * Key under which we persist the snapshot of the user's pre-existing
24
+ * `provider.neurolink` config inside `opencode.json` itself. Persisting (rather
25
+ * than relying on in-process state) means restoration still works even if the
26
+ * proxy crashes or shutdown handlers run in a different process.
27
+ *
28
+ * Mirrors the Claude pattern (`__proxy_original_env` inside Claude's settings).
29
+ */
30
+ const OPENCODE_ORIGINAL_KEY = "__proxy_original_neurolink";
31
+ export async function setOpenCodeProxySettings(baseUrl, proxyKey) {
32
+ const fs = await import("fs");
33
+ const configDir = getOpenCodeConfigDir();
34
+ try {
35
+ fs.accessSync(configDir);
36
+ }
37
+ catch {
38
+ // OpenCode not installed — config directory does not exist. Report the
39
+ // skip so the caller does not print a success message for work that did
40
+ // not happen.
41
+ return false;
42
+ }
43
+ let config;
44
+ try {
45
+ config = JSON.parse(fs.readFileSync(getOpenCodeConfigPath(), "utf8"));
46
+ }
47
+ catch {
48
+ // file missing/invalid — create fresh config object
49
+ config = { provider: {} };
50
+ }
51
+ const provider = (config.provider ?? {});
52
+ // Persist a snapshot of the user's pre-existing provider.neurolink — but
53
+ // only the first time we touch the file. Subsequent set() calls must NOT
54
+ // overwrite the snapshot (otherwise after the proxy writes its own block,
55
+ // the next set() would store the proxy's block as the "original" and
56
+ // permanently lose the user's real config on the next clear()).
57
+ if (!(OPENCODE_ORIGINAL_KEY in config)) {
58
+ config[OPENCODE_ORIGINAL_KEY] =
59
+ "neurolink" in provider
60
+ ? JSON.parse(JSON.stringify(provider.neurolink))
61
+ : null;
62
+ }
63
+ provider.neurolink = {
64
+ id: "neurolink",
65
+ name: "NeuroLink Proxy",
66
+ npm: "@ai-sdk/openai-compatible",
67
+ env: [],
68
+ models: {},
69
+ options: {
70
+ baseURL: baseUrl,
71
+ apiKey: proxyKey || "neurolink-proxy",
72
+ },
73
+ };
74
+ config.provider = provider;
75
+ fs.writeFileSync(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
76
+ return true;
77
+ }
78
+ export async function clearOpenCodeProxySettings(expectedBaseUrl) {
79
+ const fs = await import("fs");
80
+ let config;
81
+ try {
82
+ config = JSON.parse(fs.readFileSync(getOpenCodeConfigPath(), "utf8"));
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ const provider = config.provider;
88
+ if (!provider || !("neurolink" in provider)) {
89
+ return false;
90
+ }
91
+ // Check if our proxy URL matches before removing
92
+ const existing = provider.neurolink;
93
+ if (expectedBaseUrl && existing) {
94
+ const options = existing.options;
95
+ if (options && typeof options.baseURL === "string") {
96
+ if (options.baseURL !== expectedBaseUrl) {
97
+ // User configured a different URL; do not clobber
98
+ return false;
99
+ }
100
+ }
101
+ }
102
+ const hadNeurolink = "neurolink" in provider;
103
+ // Restore from the snapshot persisted at first set(), regardless of process
104
+ // identity. Only delete provider.neurolink when the snapshot says the user
105
+ // explicitly had no entry before — never on an "undefined" snapshot, since
106
+ // that would mean the snapshot was lost and we cannot prove the entry is ours.
107
+ if (OPENCODE_ORIGINAL_KEY in config) {
108
+ const snapshot = config[OPENCODE_ORIGINAL_KEY];
109
+ if (snapshot === null) {
110
+ // User had no provider.neurolink before the proxy started — safe to remove.
111
+ delete provider.neurolink;
112
+ }
113
+ else {
114
+ provider.neurolink = snapshot;
115
+ }
116
+ delete config[OPENCODE_ORIGINAL_KEY];
117
+ }
118
+ else {
119
+ // No snapshot present — refuse to delete to avoid destroying a config
120
+ // the proxy may not own (e.g. a user wrote their own `neurolink` block
121
+ // before the snapshot key was introduced, or this is being cleared from
122
+ // a process that never ran set()).
123
+ logger.debug("[proxy] OpenCode clear: no original-provider snapshot found, leaving provider.neurolink intact");
124
+ return false;
125
+ }
126
+ config.provider = provider;
127
+ fs.writeFileSync(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
128
+ return hadNeurolink;
129
+ }
130
+ /**
131
+ * Test-only export (CLAUDE.md rule 15 determinism exception). The OpenCode
132
+ * client writers resolve paths from the environment and are only reachable
133
+ * from `proxy start` / `proxy setup`, neither of which can be driven against a
134
+ * throwaway HOME without starting a real server. Consumed by
135
+ * test/continuous-test-suite-proxy.ts.
136
+ */
137
+ export const __openCodeTestHooks = {
138
+ getOpenCodeConfigDir,
139
+ getOpenCodeConfigPath,
140
+ setOpenCodeProxySettings,
141
+ clearOpenCodeProxySettings,
142
+ };
143
+ export const openCodeConfigurator = {
144
+ id: "opencode",
145
+ displayName: "OpenCode",
146
+ detect: async () => {
147
+ const fs = await import("fs");
148
+ try {
149
+ fs.accessSync(getOpenCodeConfigDir());
150
+ return true;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ },
156
+ // OpenCode speaks OpenAI Chat Completions, so it points at the /v1 door
157
+ // rather than the proxy root. The suffix belongs to the client, not the
158
+ // caller — every call site used to have to remember it.
159
+ apply: (proxyBaseUrl) => setOpenCodeProxySettings(`${proxyBaseUrl}/v1`),
160
+ restore: (proxyBaseUrl) => clearOpenCodeProxySettings(`${proxyBaseUrl}/v1`),
161
+ };
162
+ //# sourceMappingURL=openCode.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Qwen Code client configurator.
3
+ *
4
+ * Qwen Code is OpenAI-compatible: its `cli.js` reads `OPENAI_BASE_URL` and
5
+ * `OPENAI_API_KEY`, and its settings file carries the same pair under
6
+ * `security.auth`. It therefore needs no new proxy route — it points at the
7
+ * existing `/v1/chat/completions` door.
8
+ *
9
+ * Settings shape verified against a real `~/.qwen/settings.json` (`$version` 2)
10
+ * from `@qwen-code/qwen-code@0.17.0`:
11
+ *
12
+ * {
13
+ * "security": { "auth": {
14
+ * "selectedType": "openai", "apiKey": "...", "baseUrl": "https://..."
15
+ * } },
16
+ * "model": { "name": "..." },
17
+ * "$version": 2
18
+ * }
19
+ */
20
+ import type { CliProxyClientConfigurator } from "../../lib/types/index.js";
21
+ /**
22
+ * Resolved per call rather than at module load so `detect()` and `apply()`
23
+ * agree when HOME changes — under test, and on the `--dev` isolation path.
24
+ */
25
+ declare function getQwenConfigDir(): string;
26
+ declare function getQwenSettingsPath(): string;
27
+ export declare function setQwenProxySettings(baseUrl: string, proxyKey?: string): Promise<boolean>;
28
+ export declare function clearQwenProxySettings(expectedBaseUrl?: string): Promise<boolean>;
29
+ export declare const qwenCodeConfigurator: CliProxyClientConfigurator;
30
+ export declare const __qwenCodeTestHooks: {
31
+ getQwenConfigDir: typeof getQwenConfigDir;
32
+ getQwenSettingsPath: typeof getQwenSettingsPath;
33
+ setQwenProxySettings: typeof setQwenProxySettings;
34
+ clearQwenProxySettings: typeof clearQwenProxySettings;
35
+ };
36
+ export {};
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Qwen Code client configurator.
3
+ *
4
+ * Qwen Code is OpenAI-compatible: its `cli.js` reads `OPENAI_BASE_URL` and
5
+ * `OPENAI_API_KEY`, and its settings file carries the same pair under
6
+ * `security.auth`. It therefore needs no new proxy route — it points at the
7
+ * existing `/v1/chat/completions` door.
8
+ *
9
+ * Settings shape verified against a real `~/.qwen/settings.json` (`$version` 2)
10
+ * from `@qwen-code/qwen-code@0.17.0`:
11
+ *
12
+ * {
13
+ * "security": { "auth": {
14
+ * "selectedType": "openai", "apiKey": "...", "baseUrl": "https://..."
15
+ * } },
16
+ * "model": { "name": "..." },
17
+ * "$version": 2
18
+ * }
19
+ */
20
+ import { homedir } from "os";
21
+ import { join } from "path";
22
+ import { logger } from "../../lib/utils/logger.js";
23
+ /**
24
+ * Resolved per call rather than at module load so `detect()` and `apply()`
25
+ * agree when HOME changes — under test, and on the `--dev` isolation path.
26
+ */
27
+ function getQwenConfigDir() {
28
+ return join(homedir(), ".qwen");
29
+ }
30
+ function getQwenSettingsPath() {
31
+ return join(getQwenConfigDir(), "settings.json");
32
+ }
33
+ /**
34
+ * Key under which the user's pre-existing `security.auth` block is stashed,
35
+ * inside the settings file itself. Persisting it there (rather than in memory)
36
+ * means a restore still works after a crash, or from a different process —
37
+ * the same approach the Claude and OpenCode writers take.
38
+ */
39
+ const QWEN_ORIGINAL_KEY = "__proxy_original_qwen_auth";
40
+ function readQwenSettings(fs) {
41
+ try {
42
+ const parsed = JSON.parse(fs.readFileSync(getQwenSettingsPath(), "utf8"));
43
+ return parsed !== null && typeof parsed === "object"
44
+ ? parsed
45
+ : null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export async function setQwenProxySettings(baseUrl, proxyKey) {
52
+ const fs = await import("fs");
53
+ try {
54
+ fs.accessSync(getQwenConfigDir());
55
+ }
56
+ catch {
57
+ // Qwen Code not installed — report the skip so no caller prints a success
58
+ // message for work that did not happen.
59
+ return false;
60
+ }
61
+ const settings = readQwenSettings(fs) ?? {};
62
+ const security = (settings.security ?? {});
63
+ const auth = (security.auth ?? {});
64
+ // Snapshot only on first touch. A second apply() must not overwrite the
65
+ // snapshot with our own block, which would lose the user's real config on
66
+ // the next restore.
67
+ if (!(QWEN_ORIGINAL_KEY in settings)) {
68
+ settings[QWEN_ORIGINAL_KEY] =
69
+ "auth" in security ? JSON.parse(JSON.stringify(auth)) : null;
70
+ }
71
+ auth.selectedType = "openai";
72
+ auth.baseUrl = baseUrl;
73
+ auth.apiKey = proxyKey || "neurolink-proxy";
74
+ security.auth = auth;
75
+ settings.security = security;
76
+ fs.writeFileSync(getQwenSettingsPath(), JSON.stringify(settings, null, 2));
77
+ return true;
78
+ }
79
+ export async function clearQwenProxySettings(expectedBaseUrl) {
80
+ const fs = await import("fs");
81
+ const settings = readQwenSettings(fs);
82
+ if (!settings) {
83
+ return false;
84
+ }
85
+ const security = settings.security;
86
+ const auth = security?.auth;
87
+ if (!security || !auth) {
88
+ return false;
89
+ }
90
+ // The user may have pointed Qwen somewhere else since the proxy started.
91
+ // Never clobber a base URL we did not write.
92
+ if (expectedBaseUrl &&
93
+ typeof auth.baseUrl === "string" &&
94
+ auth.baseUrl !== expectedBaseUrl) {
95
+ return false;
96
+ }
97
+ if (!(QWEN_ORIGINAL_KEY in settings)) {
98
+ // No snapshot means we cannot prove this block is ours. Leaving a stale
99
+ // proxy URL behind is recoverable; destroying a real credential is not.
100
+ logger.debug("[proxy] Qwen clear: no original-auth snapshot found, leaving security.auth intact");
101
+ return false;
102
+ }
103
+ const snapshot = settings[QWEN_ORIGINAL_KEY];
104
+ if (snapshot === null) {
105
+ delete security.auth;
106
+ }
107
+ else {
108
+ security.auth = snapshot;
109
+ }
110
+ delete settings[QWEN_ORIGINAL_KEY];
111
+ settings.security = security;
112
+ fs.writeFileSync(getQwenSettingsPath(), JSON.stringify(settings, null, 2));
113
+ return true;
114
+ }
115
+ export const qwenCodeConfigurator = {
116
+ id: "qwen-code",
117
+ displayName: "Qwen Code",
118
+ detect: async () => {
119
+ const fs = await import("fs");
120
+ try {
121
+ fs.accessSync(getQwenConfigDir());
122
+ return true;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ },
128
+ // Qwen speaks OpenAI Chat Completions, so it points at the /v1 door rather
129
+ // than the proxy root.
130
+ apply: (proxyBaseUrl) => setQwenProxySettings(`${proxyBaseUrl}/v1`),
131
+ restore: (proxyBaseUrl) => clearQwenProxySettings(`${proxyBaseUrl}/v1`),
132
+ };
133
+ export const __qwenCodeTestHooks = {
134
+ getQwenConfigDir,
135
+ getQwenSettingsPath,
136
+ setQwenProxySettings,
137
+ clearQwenProxySettings,
138
+ };
139
+ //# sourceMappingURL=qwenCode.js.map
@@ -0,0 +1,19 @@
1
+ import type { CliProxyClientApplyResult, CliProxyClientConfigurator, CliProxyClientRestoreResult } from "../../lib/types/index.js";
2
+ /**
3
+ * Every CLI the proxy auto-configures, in apply order.
4
+ *
5
+ * Order is behaviour: it is the order messages appear during `proxy start`.
6
+ * Restore runs in the same order.
7
+ */
8
+ export declare const PROXY_CLIENT_CONFIGURATORS: readonly CliProxyClientConfigurator[];
9
+ /**
10
+ * Point every detected client at the proxy.
11
+ *
12
+ * One client failing must never stop the others, so each is wrapped
13
+ * independently and its error is returned rather than thrown. Callers decide
14
+ * how loudly to report — the daemon-start path logs failures at debug level
15
+ * while the setup wizard prints a visible warning.
16
+ */
17
+ export declare function applyAllClients(proxyBaseUrl: string): Promise<CliProxyClientApplyResult[]>;
18
+ /** Restore every client's previous configuration. See applyAllClients. */
19
+ export declare function restoreAllClients(proxyBaseUrl: string): Promise<CliProxyClientRestoreResult[]>;
@@ -0,0 +1,69 @@
1
+ import { claudeCodeConfigurator } from "./claudeCode.js";
2
+ import { openCodeConfigurator } from "./openCode.js";
3
+ import { codexConfigurator } from "./codex.js";
4
+ import { qwenCodeConfigurator } from "./qwenCode.js";
5
+ import { copilotConfigurator } from "./copilot.js";
6
+ /**
7
+ * Every CLI the proxy auto-configures, in apply order.
8
+ *
9
+ * Order is behaviour: it is the order messages appear during `proxy start`.
10
+ * Restore runs in the same order.
11
+ */
12
+ export const PROXY_CLIENT_CONFIGURATORS = [
13
+ claudeCodeConfigurator,
14
+ openCodeConfigurator,
15
+ codexConfigurator,
16
+ qwenCodeConfigurator,
17
+ copilotConfigurator,
18
+ ];
19
+ /**
20
+ * Point every detected client at the proxy.
21
+ *
22
+ * One client failing must never stop the others, so each is wrapped
23
+ * independently and its error is returned rather than thrown. Callers decide
24
+ * how loudly to report — the daemon-start path logs failures at debug level
25
+ * while the setup wizard prints a visible warning.
26
+ */
27
+ export async function applyAllClients(proxyBaseUrl) {
28
+ const results = [];
29
+ for (const client of PROXY_CLIENT_CONFIGURATORS) {
30
+ try {
31
+ const applied = (await client.detect())
32
+ ? await client.apply(proxyBaseUrl)
33
+ : false;
34
+ results.push({ id: client.id, displayName: client.displayName, applied });
35
+ }
36
+ catch (error) {
37
+ results.push({
38
+ id: client.id,
39
+ displayName: client.displayName,
40
+ applied: false,
41
+ error: error instanceof Error ? error : new Error(String(error)),
42
+ });
43
+ }
44
+ }
45
+ return results;
46
+ }
47
+ /** Restore every client's previous configuration. See applyAllClients. */
48
+ export async function restoreAllClients(proxyBaseUrl) {
49
+ const results = [];
50
+ for (const client of PROXY_CLIENT_CONFIGURATORS) {
51
+ try {
52
+ results.push({
53
+ id: client.id,
54
+ displayName: client.displayName,
55
+ restored: await client.restore(proxyBaseUrl),
56
+ });
57
+ }
58
+ catch (error) {
59
+ results.push({
60
+ id: client.id,
61
+ displayName: client.displayName,
62
+ restored: false,
63
+ error: error instanceof Error ? error : new Error(String(error)),
64
+ });
65
+ }
66
+ }
67
+ return results;
68
+ }
69
+ //# sourceMappingURL=registry.js.map
@@ -17,7 +17,7 @@ import { withTimeout } from "../../utils/async/index.js";
17
17
  import { estimateTokens } from "../../utils/tokenEstimation.js";
18
18
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
19
19
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
20
- import { buildGeminiResponseSchema, buildNativeConfig, collectStreamChunks, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
20
+ import { buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
21
21
  import { createStreamChannel } from "../../core/streamChannel.js";
22
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
23
23
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
@@ -1004,8 +1004,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
1004
1004
  });
1005
1005
  // Convert tools (a0269210: trust options.tools — already merged + filtered upstream)
1006
1006
  let toolsConfig;
1007
- let executeMap = new DedupExecuteMap();
1008
- let originalNameMap = new Map();
1009
1007
  let declarationsResult;
1010
1008
  const shouldUseTools = !options.disableTools;
1011
1009
  // Structured output (JSON format or schema) is incompatible with
@@ -1032,8 +1030,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
1032
1030
  const result = toNativeToolDeclarations(tools, "functionDeclarations");
1033
1031
  declarationsResult = result;
1034
1032
  toolsConfig = result.toolsConfig;
1035
- executeMap = result.executeMap;
1036
- originalNameMap = result.originalNameMap;
1037
1033
  logger.debug("[GoogleAIStudio] Converted tools for native SDK generate", {
1038
1034
  toolCount: toolsConfig[0].functionDeclarations.length,
1039
1035
  toolNames: toolsConfig[0].functionDeclarations.map((t) => t.name),
@@ -1066,114 +1062,128 @@ export class GoogleAIStudioProvider extends BaseProvider {
1066
1062
  const allToolCalls = [];
1067
1063
  const toolExecutions = [];
1068
1064
  let step = 0;
1069
- const failedTools = new Map();
1070
1065
  // Cheap reclaim trigger — see the stream twin.
1071
1066
  const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
1072
1067
  // Agentic loop for tool calling
1073
- while (step < maxSteps) {
1074
- // In-turn context guard see the stream twin.
1075
- if (step === 0 || contextGuard.shouldStop()) {
1076
- if (reclaimAiStudioContext(currentContents, modelName, contextGuard.projectedNextPromptTokens)) {
1077
- contextGuard.resetAfterReclaim();
1068
+ // Same shared engine as the streaming twin. This path has no
1069
+ // consumer channelgenerate() returns one result rather than
1070
+ // streaming so the engine's stream is drained and discarded, and
1071
+ // the turn's text comes from the result.
1072
+ const baseAdapter = createGeminiLoopAdapter({
1073
+ providerLabel: "GoogleAIStudio",
1074
+ maxSteps,
1075
+ toolFailureBreaker: { maxRetries: DEFAULT_TOOL_MAX_RETRIES },
1076
+ liveTools: options.tools ?? {},
1077
+ ...(declarationsResult ? { declarations: declarationsResult } : {}),
1078
+ buildRequest: (contents) => ({
1079
+ model: modelName,
1080
+ contents,
1081
+ config,
1082
+ ...(composedSignal
1083
+ ? { httpOptions: { signal: composedSignal } }
1084
+ : {}),
1085
+ }),
1086
+ sendStep: async (request) => client.models.generateContentStream(request),
1087
+ noteUsage: (inputTokens, outputTokens) => {
1088
+ contextGuard.noteUsage(inputTokens, outputTokens);
1089
+ },
1090
+ planReclaim: (contents, stepIndex) => {
1091
+ if (stepIndex !== 0 && !contextGuard.shouldStop()) {
1092
+ return undefined;
1078
1093
  }
1079
- }
1080
- if (composedSignal?.aborted) {
1081
- throw composedSignal.reason instanceof Error
1082
- ? composedSignal.reason
1083
- : new Error("Request aborted");
1084
- }
1085
- step++;
1086
- // Mid-turn discovery sync — see the stream twin.
1087
- if (declarationsResult) {
1088
- refreshNativeToolDeclarations(options.tools, declarationsResult);
1089
- }
1090
- logger.debug(`[GoogleAIStudio] Native SDK generate step ${step}/${maxSteps}`);
1091
- try {
1092
- const stream = await client.models.generateContentStream({
1093
- model: modelName,
1094
- contents: currentContents,
1095
- config,
1096
- ...(composedSignal
1097
- ? { httpOptions: { signal: composedSignal } }
1098
- : {}),
1099
- });
1100
- const chunkResult = await collectStreamChunks(stream);
1101
- totalInputTokens += chunkResult.inputTokens;
1102
- totalOutputTokens += chunkResult.outputTokens;
1103
- totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
1104
- totalReasoningTokens += chunkResult.reasoningTokens ?? 0;
1105
- contextGuard.noteUsage(chunkResult.inputTokens, chunkResult.outputTokens);
1106
- const stepText = extractTextFromParts(chunkResult.rawResponseParts);
1107
- // If no function calls, we're done
1108
- if (chunkResult.stepFunctionCalls.length === 0) {
1109
- finalText = stepText;
1110
- break;
1094
+ const working = [...contents];
1095
+ if (!reclaimAiStudioContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
1096
+ return undefined;
1111
1097
  }
1112
- lastStepText = stepText;
1113
- // Record tool call events on the span
1114
- for (const fc of chunkResult.stepFunctionCalls) {
1098
+ contextGuard.resetAfterReclaim();
1099
+ return working;
1100
+ },
1101
+ });
1102
+ const adapter = {
1103
+ ...baseAdapter,
1104
+ buildToolResultMessages: (contents, stepResult, toolResults) => {
1105
+ step++;
1106
+ for (const call of stepResult.toolCalls) {
1115
1107
  span.addEvent("gen_ai.tool_call", {
1116
- "tool.name": fc.name,
1108
+ "tool.name": call.name,
1117
1109
  "tool.step": step,
1118
1110
  });
1111
+ allToolCalls.push({ toolName: call.name, args: call.args });
1119
1112
  }
1120
- logger.debug(`[GoogleAIStudio] Executing ${chunkResult.stepFunctionCalls.length} function calls in generate`);
1121
- // Add model response with ALL parts (including thoughtSignature) to history
1122
- // This is critical for Gemini 3 - it requires thought signatures in subsequent turns
1123
- pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
1124
- const toolCallsBefore = allToolCalls.length;
1125
- const toolExecsBefore = toolExecutions.length;
1126
- const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, {
1127
- toolExecutions,
1128
- abortSignal: composedSignal,
1129
- originalNameMap,
1130
- liveTools: options.tools,
1131
- declarations: declarationsResult,
1132
- });
1133
- // Persist this step's tool calls/results into conversation memory.
1134
- const stepToolCalls = allToolCalls.slice(toolCallsBefore);
1135
- const stepToolExecs = toolExecutions.slice(toolExecsBefore);
1136
- if (stepToolCalls.length > 0 || stepToolExecs.length > 0) {
1137
- const stepThoughtSig = extractThoughtSignature(chunkResult.rawResponseParts);
1138
- withTimeout(this.handleToolExecutionStorage(stepToolCalls.map((tc, i) => ({
1139
- toolName: tc.toolName,
1140
- args: tc.args,
1113
+ lastStepText = stepResult.text || lastStepText;
1114
+ for (const result of toolResults) {
1115
+ toolExecutions.push({
1116
+ name: result.name,
1117
+ input: result.args,
1118
+ output: result.output,
1119
+ });
1120
+ }
1121
+ if (toolResults.length > 0) {
1122
+ const stepThoughtSig = extractThoughtSignature(stepResult.raw.rawResponseParts);
1123
+ withTimeout(this.handleToolExecutionStorage(stepResult.toolCalls.map((call, i) => ({
1124
+ toolName: call.name,
1125
+ args: call.args,
1141
1126
  ...(i === 0 && stepThoughtSig
1142
1127
  ? { thoughtSignature: stepThoughtSig }
1143
1128
  : {}),
1144
1129
  stepIndex: step,
1145
- })), stepToolExecs.map((te) => ({
1146
- toolName: te.name,
1147
- output: te.output,
1130
+ })), toolResults.map((result) => ({
1131
+ toolName: result.name,
1132
+ output: result.output,
1148
1133
  stepIndex: step,
1149
1134
  })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
1150
- logger.warn("[GoogleAIStudio] Failed to store native generate tool executions", {
1135
+ logger.warn("[GoogleAIStudio] Failed to store native tool executions", {
1151
1136
  error: error instanceof Error ? error.message : String(error),
1152
1137
  });
1153
1138
  });
1154
1139
  }
1155
- // Add function responses to history — the @google/genai SDK
1156
- // only accepts "user" and "model" as valid roles in contents.
1157
- // Function/tool responses must use role: "user" (matching the
1158
- // SDK's own automaticFunctionCalling implementation).
1159
- currentContents.push({
1160
- role: "user",
1161
- parts: functionResponses,
1162
- });
1163
- // Project this step's growth: the appended tool results ride
1164
- // the next prompt, which the provider has not reported on yet.
1140
+ const next = baseAdapter.buildToolResultMessages(contents, stepResult, toolResults);
1165
1141
  try {
1166
- contextGuard.noteAppendedChars(JSON.stringify(functionResponses).length);
1142
+ const appended = next[next.length - 1];
1143
+ contextGuard.noteAppendedChars(JSON.stringify(appended?.parts ?? []).length);
1167
1144
  }
1168
1145
  catch {
1169
1146
  /* estimation is best-effort — never break the loop */
1170
1147
  }
1148
+ return next;
1149
+ },
1150
+ };
1151
+ const engineTools = {};
1152
+ for (const [name, tool] of Object.entries(options.tools ?? {})) {
1153
+ const execute = tool?.execute;
1154
+ if (!execute) {
1155
+ continue;
1171
1156
  }
1172
- catch (error) {
1173
- logger.error("[GoogleAIStudio] Native SDK generate error", error);
1174
- throw this.handleProviderError(error);
1157
+ engineTools[name] = {
1158
+ execute: async (args, opts) => execute(args, opts),
1159
+ };
1160
+ }
1161
+ const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
1162
+ tools: engineTools,
1163
+ ...(composedSignal ? { abortSignal: composedSignal } : {}),
1164
+ });
1165
+ // Drained, not consumed: nothing streams out of generate(), but an
1166
+ // undrained channel would stall the engine mid-turn.
1167
+ const drain = (async () => {
1168
+ for await (const chunk of engineStream) {
1169
+ void chunk;
1175
1170
  }
1171
+ })();
1172
+ let engineResult;
1173
+ try {
1174
+ engineResult = await resultPromise;
1175
+ }
1176
+ catch (error) {
1177
+ await drain.catch(() => { });
1178
+ logger.error("[GoogleAIStudio] Native SDK generate error", error);
1179
+ throw this.handleProviderError(error);
1176
1180
  }
1181
+ await drain;
1182
+ totalInputTokens += engineResult.usage.inputTokens;
1183
+ totalOutputTokens += engineResult.usage.outputTokens;
1184
+ totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
1185
+ totalReasoningTokens += engineResult.usage.reasoningTokens ?? 0;
1186
+ finalText = engineResult.text;
1177
1187
  finalText = handleMaxStepsTermination("[GoogleAIStudio]", step, maxSteps, finalText, lastStepText);
1178
1188
  const responseTime = Date.now() - startTime;
1179
1189
  // Set token usage and finish reason on the span