@gajae-code/ai 0.4.4 → 0.5.0
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.
- package/CHANGELOG.md +42 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/amazon-bedrock.d.ts +29 -5
- package/dist/types/providers/composer-discipline.d.ts +27 -0
- package/dist/types/providers/cursor.d.ts +1 -1
- package/dist/types/providers/google-gemini-cli.d.ts +1 -1
- package/dist/types/providers/google-shared.d.ts +11 -1
- package/dist/types/providers/ollama.d.ts +36 -1
- package/dist/types/providers/openai-completions-compat.d.ts +3 -1
- package/dist/types/providers/register-builtins.d.ts +3 -3
- package/dist/types/stream.d.ts +2 -2
- package/dist/types/types.d.ts +29 -3
- package/dist/types/utils/event-stream.d.ts +6 -1
- package/dist/types/utils/tool-choice-capability.d.ts +41 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +5 -1
- package/src/index.ts +1 -0
- package/src/model-manager.ts +33 -1
- package/src/model-thinking.ts +9 -0
- package/src/models.json +92 -0
- package/src/models.ts +33 -7
- package/src/provider-models/openai-compat.ts +9 -1
- package/src/providers/amazon-bedrock.ts +145 -60
- package/src/providers/anthropic.ts +89 -10
- package/src/providers/azure-openai-responses.ts +44 -3
- package/src/providers/composer-discipline.ts +38 -0
- package/src/providers/cursor.ts +80 -4
- package/src/providers/google-gemini-cli.ts +69 -10
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-shared.ts +61 -12
- package/src/providers/ollama.ts +60 -4
- package/src/providers/openai-codex-responses.ts +151 -2
- package/src/providers/openai-completions-compat.ts +9 -1
- package/src/providers/openai-completions.ts +46 -6
- package/src/providers/openai-request-transform.ts +1 -0
- package/src/providers/openai-responses.ts +54 -5
- package/src/providers/register-builtins.ts +5 -6
- package/src/stream.ts +24 -19
- package/src/types.ts +41 -3
- package/src/utils/event-stream.ts +35 -5
- package/src/utils/tool-choice-capability.ts +220 -0
package/src/providers/cursor.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
30
30
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
31
31
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
32
32
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
33
|
+
import { COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
|
|
33
34
|
import type { McpToolDefinition } from "./cursor/gen/agent_pb";
|
|
34
35
|
import {
|
|
35
36
|
AgentClientMessageSchema,
|
|
@@ -569,7 +570,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
569
570
|
return stream;
|
|
570
571
|
};
|
|
571
572
|
|
|
572
|
-
type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" };
|
|
573
|
+
type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" | "native" };
|
|
573
574
|
|
|
574
575
|
interface BlockState {
|
|
575
576
|
currentTextBlock: (TextContent & { index: number }) | null;
|
|
@@ -1844,6 +1845,60 @@ function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
|
|
|
1844
1845
|
};
|
|
1845
1846
|
}
|
|
1846
1847
|
|
|
1848
|
+
// Map a cursor ToolCall oneof field name (e.g. "shellToolCall") to a display tool
|
|
1849
|
+
// name. Mirrors cli-jaw's cursorToolKindLabel (src/agent/events/cursor.ts) so the
|
|
1850
|
+
// two surfaces label cursor-native tools identically.
|
|
1851
|
+
const CURSOR_NATIVE_KIND_ALIASES: Record<string, string> = {
|
|
1852
|
+
shell: "bash",
|
|
1853
|
+
read: "read",
|
|
1854
|
+
write: "write",
|
|
1855
|
+
delete: "delete",
|
|
1856
|
+
edit: "edit",
|
|
1857
|
+
grep: "grep",
|
|
1858
|
+
glob: "glob",
|
|
1859
|
+
ls: "ls",
|
|
1860
|
+
semSearch: "codebase_search",
|
|
1861
|
+
webSearch: "web_search",
|
|
1862
|
+
fetch: "fetch",
|
|
1863
|
+
task: "task",
|
|
1864
|
+
createPlan: "create_plan",
|
|
1865
|
+
askQuestion: "ask_question",
|
|
1866
|
+
readLints: "read_lints",
|
|
1867
|
+
applyAgentDiff: "apply_diff",
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
function cursorNativeToolName(kindKey: string): string {
|
|
1871
|
+
const base = kindKey.replace(/ToolCall$/i, "");
|
|
1872
|
+
if (!base) return "tool";
|
|
1873
|
+
return CURSOR_NATIVE_KIND_ALIASES[base] ?? base;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// Cursor's model sometimes calls its own native IDE tools (shell/glob/grep/…)
|
|
1877
|
+
// instead of the advertised MCP tools. Those arrive as ToolCall oneof variants we
|
|
1878
|
+
// do not otherwise handle (everything except mcpToolCall / updateTodosToolCall), so
|
|
1879
|
+
// without this they are silently dropped and never render. Build a generic toolCall
|
|
1880
|
+
// block from whichever *ToolCall field is set so the call (and its result) is shown.
|
|
1881
|
+
function buildNativeToolCallBlock(
|
|
1882
|
+
toolCall: Record<string, unknown>,
|
|
1883
|
+
callId: string,
|
|
1884
|
+
index: number,
|
|
1885
|
+
): ToolCallState | null {
|
|
1886
|
+
for (const [key, payload] of Object.entries(toolCall)) {
|
|
1887
|
+
if (!/ToolCall$/.test(key) || !payload || typeof payload !== "object") continue;
|
|
1888
|
+
if (key === "mcpToolCall" || key === "updateTodosToolCall") continue;
|
|
1889
|
+
const args = (payload as { args?: unknown }).args;
|
|
1890
|
+
return {
|
|
1891
|
+
type: "toolCall",
|
|
1892
|
+
id: callId,
|
|
1893
|
+
name: cursorNativeToolName(key),
|
|
1894
|
+
arguments: args && typeof args === "object" ? (args as Record<string, unknown>) : { raw: payload },
|
|
1895
|
+
index,
|
|
1896
|
+
kind: "native",
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1847
1902
|
function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
|
|
1848
1903
|
if (toolResult.isError) {
|
|
1849
1904
|
return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
|
|
@@ -1987,6 +2042,21 @@ function processInteractionUpdate(
|
|
|
1987
2042
|
output.content.push(block);
|
|
1988
2043
|
state.setToolCall(block);
|
|
1989
2044
|
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Fallback: cursor-native tool variants (shell/glob/grep/…) we don't model
|
|
2049
|
+
// explicitly. Render them so the call and its result are visible instead of
|
|
2050
|
+
// vanishing.
|
|
2051
|
+
const nativeBlock = buildNativeToolCallBlock(
|
|
2052
|
+
toolCall,
|
|
2053
|
+
update.message.value.callId || crypto.randomUUID(),
|
|
2054
|
+
output.content.length,
|
|
2055
|
+
);
|
|
2056
|
+
if (nativeBlock) {
|
|
2057
|
+
output.content.push(nativeBlock);
|
|
2058
|
+
state.setToolCall(nativeBlock);
|
|
2059
|
+
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
1990
2060
|
}
|
|
1991
2061
|
}
|
|
1992
2062
|
} else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
|
|
@@ -2215,12 +2285,18 @@ function findLastUserMessageIndex(messages: Message[]): number {
|
|
|
2215
2285
|
* When no system prompts are provided, returns a single default greeting so we never emit
|
|
2216
2286
|
* an empty `rootPromptMessagesJson` head.
|
|
2217
2287
|
*/
|
|
2218
|
-
export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined): string[] {
|
|
2288
|
+
export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined, modelId?: string): string[] {
|
|
2219
2289
|
const systemPrompts = normalizeSystemPrompts(systemPrompt);
|
|
2220
2290
|
if (systemPrompts.length === 0) {
|
|
2221
2291
|
return [JSON.stringify({ role: "system", content: "You are a helpful assistant." })];
|
|
2222
2292
|
}
|
|
2223
|
-
|
|
2293
|
+
const jsons = systemPrompts.map(content => JSON.stringify({ role: "system", content }));
|
|
2294
|
+
// Composer-harness models need anchor/edit discipline pinned ahead of the
|
|
2295
|
+
// host prompt (see composer-discipline.ts for the observed failure modes).
|
|
2296
|
+
if (modelId !== undefined && isComposerHarnessModel(modelId)) {
|
|
2297
|
+
jsons.unshift(JSON.stringify({ role: "system", content: COMPOSER_EDIT_DISCIPLINE_PROMPT }));
|
|
2298
|
+
}
|
|
2299
|
+
return jsons;
|
|
2224
2300
|
}
|
|
2225
2301
|
|
|
2226
2302
|
function buildRootPromptMessagesJson(
|
|
@@ -2432,7 +2508,7 @@ function buildGrpcRequest(
|
|
|
2432
2508
|
} {
|
|
2433
2509
|
const blobStore = state.blobStore;
|
|
2434
2510
|
|
|
2435
|
-
const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt).map(json =>
|
|
2511
|
+
const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt, model.id).map(json =>
|
|
2436
2512
|
storeCursorBlob(blobStore, new TextEncoder().encode(json)),
|
|
2437
2513
|
);
|
|
2438
2514
|
|
|
@@ -25,6 +25,11 @@ import { resolveRetryBudget } from "../utils/retry-budget";
|
|
|
25
25
|
// Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
|
|
26
26
|
// the stream provider trusts the access token threaded through `options.apiKey`.
|
|
27
27
|
import { normalizeSchemaForCCA } from "../utils/schema";
|
|
28
|
+
import {
|
|
29
|
+
isForcedToolChoiceUnsupportedError,
|
|
30
|
+
markToolChoiceIncapability,
|
|
31
|
+
resolveToolChoice,
|
|
32
|
+
} from "../utils/tool-choice-capability";
|
|
28
33
|
import { ANTIGRAVITY_SYSTEM_INSTRUCTION, getAntigravityUserAgent, getGeminiCliHeaders } from "./google-gemini-headers";
|
|
29
34
|
import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "./google-shared";
|
|
30
35
|
import {
|
|
@@ -48,7 +53,7 @@ import {
|
|
|
48
53
|
export type { GoogleThinkingLevel };
|
|
49
54
|
|
|
50
55
|
export interface GoogleGeminiCliOptions extends StreamOptions {
|
|
51
|
-
toolChoice?: "auto" | "none" | "any";
|
|
56
|
+
toolChoice?: "auto" | "none" | "any" | "required";
|
|
52
57
|
/**
|
|
53
58
|
* Thinking/reasoning configuration.
|
|
54
59
|
* - Gemini 2.x models: use `budgetTokens` to set the thinking budget
|
|
@@ -324,7 +329,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
324
329
|
...(needsClaudeThinkingBetaHeader(model) ? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER } : {}),
|
|
325
330
|
...(options?.headers ?? {}),
|
|
326
331
|
};
|
|
327
|
-
|
|
332
|
+
let requestBodyJson = JSON.stringify(requestBody);
|
|
328
333
|
rawRequestDump = {
|
|
329
334
|
provider: model.provider,
|
|
330
335
|
api: output.api,
|
|
@@ -334,7 +339,13 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
334
339
|
headers: requestHeaders,
|
|
335
340
|
};
|
|
336
341
|
|
|
337
|
-
const
|
|
342
|
+
const sentForcedToolChoice =
|
|
343
|
+
(requestBody.request.toolConfig?.functionCallingConfig?.mode === "ANY" ||
|
|
344
|
+
requestBody.request.toolConfig?.functionCallingConfig?.mode === "VALIDATED") &&
|
|
345
|
+
options?.toolChoice !== undefined &&
|
|
346
|
+
options.toolChoice !== "auto" &&
|
|
347
|
+
options.toolChoice !== "none";
|
|
348
|
+
let response = await fetchWithRetry(
|
|
338
349
|
attempt => `${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`,
|
|
339
350
|
{
|
|
340
351
|
method: "POST",
|
|
@@ -347,6 +358,49 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
347
358
|
fetch: options?.fetch,
|
|
348
359
|
},
|
|
349
360
|
);
|
|
361
|
+
if (!response.ok && sentForcedToolChoice) {
|
|
362
|
+
const errorText = await response.text();
|
|
363
|
+
const error = withHttpStatus(
|
|
364
|
+
new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`),
|
|
365
|
+
response.status,
|
|
366
|
+
);
|
|
367
|
+
if (firstTokenTime === undefined && isForcedToolChoiceUnsupportedError(error, true)) {
|
|
368
|
+
const beforeMark = resolveToolChoice(model, options?.toolChoice);
|
|
369
|
+
markToolChoiceIncapability(model, "auto", error.message);
|
|
370
|
+
stream.push({
|
|
371
|
+
type: "toolChoiceIncapability",
|
|
372
|
+
api: model.api,
|
|
373
|
+
provider: model.provider,
|
|
374
|
+
model: model.id,
|
|
375
|
+
requestedLevel: beforeMark.requestedLevel,
|
|
376
|
+
resolvedLevel: "auto",
|
|
377
|
+
reason: error.message,
|
|
378
|
+
registryKey: beforeMark.registryKey,
|
|
379
|
+
});
|
|
380
|
+
requestBody = {
|
|
381
|
+
...requestBody,
|
|
382
|
+
request: { ...requestBody.request, toolConfig: undefined },
|
|
383
|
+
};
|
|
384
|
+
requestBodyJson = JSON.stringify(requestBody);
|
|
385
|
+
rawRequestDump = { ...rawRequestDump, body: requestBody };
|
|
386
|
+
response = await fetchWithRetry(
|
|
387
|
+
attempt =>
|
|
388
|
+
`${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`,
|
|
389
|
+
{
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: requestHeaders,
|
|
392
|
+
body: requestBodyJson,
|
|
393
|
+
signal: options?.signal,
|
|
394
|
+
maxAttempts: 1,
|
|
395
|
+
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
396
|
+
maxDelayMs: options?.maxRetryDelayMs ?? RATE_LIMIT_BUDGET_MS,
|
|
397
|
+
fetch: options?.fetch,
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
350
404
|
if (!response.ok) {
|
|
351
405
|
const errorText = await response.text();
|
|
352
406
|
throw withHttpStatus(
|
|
@@ -743,13 +797,15 @@ export function buildRequest(
|
|
|
743
797
|
request.generationConfig = generationConfig;
|
|
744
798
|
}
|
|
745
799
|
|
|
800
|
+
const resolvedToolChoice = resolveToolChoice(model, options.toolChoice);
|
|
746
801
|
if (context.tools && context.tools.length > 0) {
|
|
747
802
|
const convertedTools = convertTools(context.tools, model);
|
|
748
803
|
request.tools = isAntigravity ? normalizeAntigravityTools(convertedTools) : convertedTools;
|
|
749
|
-
|
|
804
|
+
const resolved = resolvedToolChoice;
|
|
805
|
+
if (resolved.resolvedChoice) {
|
|
750
806
|
request.toolConfig = {
|
|
751
807
|
functionCallingConfig: {
|
|
752
|
-
mode: mapToolChoice(
|
|
808
|
+
mode: mapToolChoice(resolved.resolvedChoice as string),
|
|
753
809
|
},
|
|
754
810
|
};
|
|
755
811
|
}
|
|
@@ -763,11 +819,14 @@ export function buildRequest(
|
|
|
763
819
|
}
|
|
764
820
|
|
|
765
821
|
if (isAntigravity && isClaudeModel(model.id)) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
822
|
+
const resolvedLevel = resolvedToolChoice.resolvedLevel;
|
|
823
|
+
if (resolvedLevel === "named" || resolvedLevel === "required") {
|
|
824
|
+
request.toolConfig = {
|
|
825
|
+
functionCallingConfig: {
|
|
826
|
+
mode: "VALIDATED" as FunctionCallingConfigMode,
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
}
|
|
771
830
|
}
|
|
772
831
|
|
|
773
832
|
if (isAntigravity && shouldInjectAntigravitySystemInstruction(model.id)) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
|
|
5
5
|
*/
|
|
6
6
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
7
|
-
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.
|
|
7
|
+
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.46.0";
|
|
8
8
|
const platform = process.platform === "win32" ? "win32" : process.platform;
|
|
9
9
|
const arch = process.arch === "x64" ? "x64" : process.arch;
|
|
10
10
|
return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
|
|
@@ -22,6 +22,11 @@ import { normalizeSystemPrompts } from "../utils";
|
|
|
22
22
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
23
23
|
import { finalizeErrorMessage, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
24
24
|
import { normalizeSchemaForCCA, normalizeSchemaForGoogle, toolWireSchema } from "../utils/schema";
|
|
25
|
+
import {
|
|
26
|
+
isForcedToolChoiceUnsupportedError,
|
|
27
|
+
markToolChoiceIncapability,
|
|
28
|
+
resolveToolChoice,
|
|
29
|
+
} from "../utils/tool-choice-capability";
|
|
25
30
|
import type {
|
|
26
31
|
Content,
|
|
27
32
|
FinishReason,
|
|
@@ -59,7 +64,7 @@ export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LO
|
|
|
59
64
|
* `google-gemini-cli` uses a different transport and request shape — do not extend this for it.
|
|
60
65
|
*/
|
|
61
66
|
export interface GoogleSharedStreamOptions extends StreamOptions {
|
|
62
|
-
toolChoice?: "auto" | "none" | "any";
|
|
67
|
+
toolChoice?: "auto" | "none" | "any" | "required";
|
|
63
68
|
thinking?: {
|
|
64
69
|
enabled: boolean;
|
|
65
70
|
budgetTokens?: number;
|
|
@@ -366,6 +371,7 @@ export function mapToolChoice(choice: string): FunctionCallingConfigMode {
|
|
|
366
371
|
case "none":
|
|
367
372
|
return "NONE";
|
|
368
373
|
case "any":
|
|
374
|
+
case "required":
|
|
369
375
|
return "ANY";
|
|
370
376
|
default:
|
|
371
377
|
return "AUTO";
|
|
@@ -689,14 +695,15 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
|
|
|
689
695
|
...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools, model) }),
|
|
690
696
|
};
|
|
691
697
|
|
|
692
|
-
if (context.tools && context.tools.length > 0
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
698
|
+
if (context.tools && context.tools.length > 0) {
|
|
699
|
+
const resolved = resolveToolChoice(model, options.toolChoice);
|
|
700
|
+
if (resolved.resolvedChoice) {
|
|
701
|
+
config.toolConfig = {
|
|
702
|
+
functionCallingConfig: {
|
|
703
|
+
mode: mapToolChoice(resolved.resolvedChoice as string),
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
700
707
|
}
|
|
701
708
|
|
|
702
709
|
if (options.thinking?.enabled && model.reasoning) {
|
|
@@ -789,14 +796,56 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
789
796
|
headers: plan.headers,
|
|
790
797
|
};
|
|
791
798
|
|
|
792
|
-
|
|
799
|
+
let wireBody = paramsToWireBody(params);
|
|
800
|
+
const sentForcedToolChoice =
|
|
801
|
+
(params.config?.toolConfig?.functionCallingConfig?.mode === "ANY" ||
|
|
802
|
+
params.config?.toolConfig?.functionCallingConfig?.mode === "VALIDATED") &&
|
|
803
|
+
options?.toolChoice !== undefined &&
|
|
804
|
+
options.toolChoice !== "auto" &&
|
|
805
|
+
options.toolChoice !== "none";
|
|
793
806
|
const fetchImpl = plan.fetch ?? options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
|
|
794
|
-
|
|
807
|
+
let response = await fetchImpl(plan.url, {
|
|
795
808
|
method: "POST",
|
|
796
809
|
headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
797
810
|
body: JSON.stringify(wireBody),
|
|
798
811
|
signal: options?.signal,
|
|
799
812
|
});
|
|
813
|
+
if (!response.ok && sentForcedToolChoice) {
|
|
814
|
+
const errorText = await response.text().catch(() => "");
|
|
815
|
+
const error = withHttpStatus(
|
|
816
|
+
new Error(`Google API error (${response.status}): ${extractGoogleErrorMessage(errorText)}`),
|
|
817
|
+
response.status,
|
|
818
|
+
);
|
|
819
|
+
if (firstTokenTime === undefined && isForcedToolChoiceUnsupportedError(error, true)) {
|
|
820
|
+
const beforeMark = resolveToolChoice(model, options?.toolChoice);
|
|
821
|
+
markToolChoiceIncapability(model, "auto", error.message);
|
|
822
|
+
stream.push({
|
|
823
|
+
type: "toolChoiceIncapability",
|
|
824
|
+
api: model.api,
|
|
825
|
+
provider: model.provider,
|
|
826
|
+
model: model.id,
|
|
827
|
+
requestedLevel: beforeMark.requestedLevel,
|
|
828
|
+
resolvedLevel: "auto",
|
|
829
|
+
reason: error.message,
|
|
830
|
+
registryKey: beforeMark.registryKey,
|
|
831
|
+
});
|
|
832
|
+
const retryParams = {
|
|
833
|
+
...params,
|
|
834
|
+
config: params.config ? { ...params.config, toolConfig: undefined } : params.config,
|
|
835
|
+
};
|
|
836
|
+
params = retryParams;
|
|
837
|
+
rawRequestDump = { ...rawRequestDump, body: params };
|
|
838
|
+
wireBody = paramsToWireBody(params);
|
|
839
|
+
response = await fetchImpl(plan.url, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
842
|
+
body: JSON.stringify(wireBody),
|
|
843
|
+
signal: options?.signal,
|
|
844
|
+
});
|
|
845
|
+
} else {
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
800
849
|
if (!response.ok) {
|
|
801
850
|
const errorText = await response.text().catch(() => "");
|
|
802
851
|
throw withHttpStatus(
|
|
@@ -857,7 +906,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
857
906
|
* `abortSignal` is intentionally dropped — the SDK propagates it via `fetch.signal`,
|
|
858
907
|
* which our caller already wires up through `options.signal`.
|
|
859
908
|
*/
|
|
860
|
-
function paramsToWireBody(params: GenerateContentParameters): Record<string, unknown> {
|
|
909
|
+
export function paramsToWireBody(params: GenerateContentParameters): Record<string, unknown> {
|
|
861
910
|
const body: Record<string, unknown> = { contents: params.contents };
|
|
862
911
|
const config = params.config;
|
|
863
912
|
if (!config) return body;
|
package/src/providers/ollama.ts
CHANGED
|
@@ -20,6 +20,11 @@ import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-ins
|
|
|
20
20
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
21
21
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
22
22
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
23
|
+
import {
|
|
24
|
+
isForcedToolChoiceUnsupportedError,
|
|
25
|
+
markToolChoiceIncapability,
|
|
26
|
+
resolveToolChoice,
|
|
27
|
+
} from "../utils/tool-choice-capability";
|
|
23
28
|
import { transformMessages } from "./transform-messages";
|
|
24
29
|
|
|
25
30
|
export interface OllamaChatOptions extends StreamOptions {
|
|
@@ -253,10 +258,22 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
253
258
|
}));
|
|
254
259
|
}
|
|
255
260
|
|
|
256
|
-
function createChatBody(model: Model<"ollama-chat">, context: Context, options: OllamaChatOptions | undefined) {
|
|
261
|
+
export function createChatBody(model: Model<"ollama-chat">, context: Context, options: OllamaChatOptions | undefined) {
|
|
257
262
|
const think = mapReasoning(options?.reasoning);
|
|
258
|
-
const
|
|
259
|
-
const
|
|
263
|
+
const resolved = resolveToolChoice(model, options?.toolChoice);
|
|
264
|
+
const toolChoice = mapToolChoice(resolved.resolvedChoice);
|
|
265
|
+
|
|
266
|
+
// Ollama's wire protocol has no named forcing — a named request rides
|
|
267
|
+
// `tool_choice: "required"` with the tool list narrowed to the target
|
|
268
|
+
// (issue #1236). Narrow whenever the resolved choice still forces a tool.
|
|
269
|
+
const shouldNarrowToNamedTool =
|
|
270
|
+
resolved.requestedLevel === "named" &&
|
|
271
|
+
(resolved.resolvedLevel === "named" || resolved.resolvedLevel === "required") &&
|
|
272
|
+
resolved.targetToolName !== undefined;
|
|
273
|
+
const selectedTools =
|
|
274
|
+
shouldNarrowToNamedTool && resolved.targetToolName
|
|
275
|
+
? selectToolsForToolChoice(context.tools, { type: "tool", name: resolved.targetToolName })
|
|
276
|
+
: context.tools;
|
|
260
277
|
const tools = convertTools(selectedTools);
|
|
261
278
|
return {
|
|
262
279
|
model: model.id,
|
|
@@ -382,6 +399,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
382
399
|
}
|
|
383
400
|
const baseUrl = normalizeBaseUrl(model.baseUrl);
|
|
384
401
|
let body = createChatBody(model, context, options);
|
|
402
|
+
const sentForcedToolChoice = body.tool_choice === "required";
|
|
385
403
|
const replacementPayload = await options.onPayload?.(body, model);
|
|
386
404
|
if (replacementPayload !== undefined) {
|
|
387
405
|
body = replacementPayload as typeof body;
|
|
@@ -394,7 +412,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
394
412
|
url: `${baseUrl}/api/chat`,
|
|
395
413
|
body,
|
|
396
414
|
};
|
|
397
|
-
|
|
415
|
+
let response = await fetchWithRetry(`${baseUrl}/api/chat`, {
|
|
398
416
|
method: "POST",
|
|
399
417
|
headers: {
|
|
400
418
|
...model.headers,
|
|
@@ -408,6 +426,44 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
408
426
|
defaultDelayMs: OLLAMA_RETRY_DELAYS_MS,
|
|
409
427
|
fetch: options.fetch,
|
|
410
428
|
});
|
|
429
|
+
if (!response.ok && sentForcedToolChoice) {
|
|
430
|
+
const error = new Error(
|
|
431
|
+
`HTTP ${response.status} from ${baseUrl}/api/chat: ${await response.text().catch(() => "")}`,
|
|
432
|
+
);
|
|
433
|
+
(error as Error & { status?: number }).status = response.status;
|
|
434
|
+
if (firstTokenTime === undefined && isForcedToolChoiceUnsupportedError(error, true)) {
|
|
435
|
+
markToolChoiceIncapability(model, "auto", error.message);
|
|
436
|
+
stream.push({
|
|
437
|
+
type: "toolChoiceIncapability",
|
|
438
|
+
api: model.api,
|
|
439
|
+
provider: model.provider,
|
|
440
|
+
model: model.id,
|
|
441
|
+
requestedLevel: "required",
|
|
442
|
+
resolvedLevel: "auto",
|
|
443
|
+
reason: error.message,
|
|
444
|
+
registryKey: resolveToolChoice(model, options?.toolChoice).registryKey,
|
|
445
|
+
});
|
|
446
|
+
body = { ...body };
|
|
447
|
+
delete (body as { tool_choice?: unknown }).tool_choice;
|
|
448
|
+
rawRequestDump = { ...rawRequestDump, body };
|
|
449
|
+
response = await fetchWithRetry(`${baseUrl}/api/chat`, {
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: {
|
|
452
|
+
...model.headers,
|
|
453
|
+
...options.headers,
|
|
454
|
+
Authorization: `Bearer ${apiKey}`,
|
|
455
|
+
"Content-Type": "application/json",
|
|
456
|
+
},
|
|
457
|
+
body: JSON.stringify(body),
|
|
458
|
+
signal: options.signal,
|
|
459
|
+
maxAttempts: 1,
|
|
460
|
+
defaultDelayMs: OLLAMA_RETRY_DELAYS_MS,
|
|
461
|
+
fetch: options.fetch,
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
411
467
|
if (!response.ok) {
|
|
412
468
|
throw new Error(`HTTP ${response.status} from ${baseUrl}/api/chat`);
|
|
413
469
|
}
|