@gajae-code/ai 0.4.5 → 0.5.1
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 +31 -1
- package/dist/types/index.d.ts +2 -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/types.d.ts +25 -3
- package/dist/types/usage/grok-cli.d.ts +10 -0
- package/dist/types/utils/event-stream.d.ts +6 -1
- package/dist/types/utils/oauth/xai.d.ts +10 -3
- package/dist/types/utils/tool-choice-capability.d.ts +41 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +3 -0
- package/src/index.ts +2 -0
- package/src/model-thinking.ts +9 -0
- package/src/models.json +116 -0
- package/src/models.ts +33 -7
- package/src/provider-models/descriptors.ts +1 -1
- package/src/provider-models/openai-compat.ts +9 -1
- package/src/providers/amazon-bedrock.ts +145 -60
- package/src/providers/anthropic.ts +85 -32
- package/src/providers/azure-openai-responses.ts +44 -3
- package/src/providers/composer-discipline.ts +38 -0
- package/src/providers/cursor.ts +10 -3
- package/src/providers/google-gemini-cli.ts +69 -10
- 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/rate-limit-utils.ts +11 -2
- package/src/types.ts +37 -3
- package/src/usage/grok-cli.ts +163 -0
- package/src/utils/event-stream.ts +35 -5
- package/src/utils/oauth/xai.ts +49 -13
- package/src/utils/tool-choice-capability.ts +220 -0
package/src/models.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { enrichModelThinking } from "./model-thinking";
|
|
2
2
|
import MODELS from "./models.json" with { type: "json" };
|
|
3
3
|
import type { Api, KnownProvider, Model, Usage } from "./types";
|
|
4
|
+
import { isClaudeForcedToolChoiceIncapableModelId } from "./utils/tool-choice-capability";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Static bundled model registry loaded from `models.json`.
|
|
@@ -10,28 +11,53 @@ import type { Api, KnownProvider, Model, Usage } from "./types";
|
|
|
10
11
|
*
|
|
11
12
|
* For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`.
|
|
12
13
|
*/
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const providerNames = Object.keys(MODELS) as KnownProvider[];
|
|
15
|
+
const providerModelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
|
|
16
|
+
|
|
17
|
+
function getProviderModels(provider: GeneratedProvider): Map<string, Model<Api>> | undefined {
|
|
18
|
+
const cached = providerModelRegistry.get(provider);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
const models = MODELS[provider];
|
|
21
|
+
if (!models) return undefined;
|
|
15
22
|
const providerModels = new Map<string, Model<Api>>();
|
|
16
23
|
for (const [id, model] of Object.entries(models)) {
|
|
17
|
-
providerModels.set(id, enrichModelThinking(model as Model<Api>));
|
|
24
|
+
providerModels.set(id, applyBundledCompatDefaults(enrichModelThinking(model as Model<Api>)));
|
|
25
|
+
}
|
|
26
|
+
providerModelRegistry.set(provider, providerModels);
|
|
27
|
+
return providerModels;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bundled-catalog compat defaults applied at load time so stale committed
|
|
32
|
+
* models.json snapshots still receive policy-critical fields (e.g. Claude
|
|
33
|
+
* Mythos rejecting forced tool use) without a full regeneration.
|
|
34
|
+
*/
|
|
35
|
+
function applyBundledCompatDefaults(model: Model<Api>): Model<Api> {
|
|
36
|
+
if (
|
|
37
|
+
(model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") &&
|
|
38
|
+
isClaudeForcedToolChoiceIncapableModelId(model.id) &&
|
|
39
|
+
(model.compat as { toolChoiceSupport?: string } | undefined)?.toolChoiceSupport === undefined
|
|
40
|
+
) {
|
|
41
|
+
return { ...model, compat: { ...(model.compat ?? {}), toolChoiceSupport: "auto" } as Model<Api>["compat"] };
|
|
18
42
|
}
|
|
19
|
-
|
|
43
|
+
return model;
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
export type GeneratedProvider = keyof typeof MODELS;
|
|
23
47
|
|
|
24
48
|
export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi> {
|
|
25
|
-
const providerModels =
|
|
49
|
+
const providerModels = getProviderModels(provider);
|
|
26
50
|
return providerModels?.get(modelId) as Model<TApi>;
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
export function getBundledProviders(): KnownProvider[] {
|
|
30
|
-
|
|
54
|
+
// Defensive copy: the old eager path returned a fresh Array.from(...), so
|
|
55
|
+
// callers may freely mutate their result without corrupting enumeration.
|
|
56
|
+
return providerNames.slice();
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
export function getBundledModels(provider: GeneratedProvider): Model<Api>[] {
|
|
34
|
-
const models =
|
|
60
|
+
const models = getProviderModels(provider);
|
|
35
61
|
return models ? (Array.from(models.values()) as Model<Api>[]) : [];
|
|
36
62
|
}
|
|
37
63
|
|
|
@@ -280,7 +280,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
280
280
|
config => zenmuxModelManagerOptions(config),
|
|
281
281
|
catalog("ZenMux", ["ZENMUX_API_KEY"]),
|
|
282
282
|
),
|
|
283
|
-
catalogDescriptor("zai", "glm-5.
|
|
283
|
+
catalogDescriptor("zai", "glm-5.2", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
|
|
284
284
|
descriptor("github-copilot", "gpt-4o", config => githubCopilotModelManagerOptions(config)),
|
|
285
285
|
descriptor("google", "gemini-2.5-pro", config => googleModelManagerOptions(config)),
|
|
286
286
|
catalogDescriptor(
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../utils/discovery/openai-compatible";
|
|
12
12
|
import { toFireworksPublicModelId } from "../utils/fireworks-model-id";
|
|
13
13
|
import { getGitHubCopilotBaseUrl, OPENCODE_HEADERS, parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
14
|
+
import { isClaudeForcedToolChoiceIncapableModelId } from "../utils/tool-choice-capability";
|
|
14
15
|
import { createBundledReferenceMap, createReferenceResolver } from "./bundled-references";
|
|
15
16
|
|
|
16
17
|
const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
@@ -68,6 +69,9 @@ async function fetchModelsDevPayload(fetchImpl: typeof fetch = fetch): Promise<u
|
|
|
68
69
|
return response.json();
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function anthropicToolChoiceCompat(modelId: string): Pick<Model<"anthropic-messages">, "compat"> {
|
|
73
|
+
return isClaudeForcedToolChoiceIncapableModelId(modelId) ? { compat: { toolChoiceSupport: "auto" } } : {};
|
|
74
|
+
}
|
|
71
75
|
function mapAnthropicModelsDev(payload: unknown, baseUrl: string): Model<"anthropic-messages">[] {
|
|
72
76
|
if (!isRecord(payload)) {
|
|
73
77
|
return [];
|
|
@@ -106,6 +110,7 @@ function mapAnthropicModelsDev(payload: unknown, baseUrl: string): Model<"anthro
|
|
|
106
110
|
},
|
|
107
111
|
contextWindow: toPositiveNumber(model.limit?.context, UNK_CONTEXT_WINDOW),
|
|
108
112
|
maxTokens: toPositiveNumber(model.limit?.output, UNK_MAX_TOKENS),
|
|
113
|
+
...anthropicToolChoiceCompat(modelId),
|
|
109
114
|
});
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -141,7 +146,10 @@ function buildAnthropicReferenceMap(
|
|
|
141
146
|
(model): model is Model<"anthropic-messages"> => model.api === "anthropic-messages",
|
|
142
147
|
);
|
|
143
148
|
for (const model of bundledModels) {
|
|
144
|
-
merged.set(model.id,
|
|
149
|
+
merged.set(model.id, {
|
|
150
|
+
...model,
|
|
151
|
+
compat: { ...(model.compat ?? {}), ...anthropicToolChoiceCompat(model.id).compat },
|
|
152
|
+
});
|
|
145
153
|
}
|
|
146
154
|
return merged;
|
|
147
155
|
}
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
ThinkingContent,
|
|
26
26
|
Tool,
|
|
27
27
|
ToolCall,
|
|
28
|
+
ToolChoice,
|
|
28
29
|
ToolResultMessage,
|
|
29
30
|
} from "../types";
|
|
30
31
|
import { normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
@@ -33,6 +34,11 @@ import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus
|
|
|
33
34
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
34
35
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
35
36
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
37
|
+
import {
|
|
38
|
+
isForcedToolChoiceUnsupportedError,
|
|
39
|
+
markToolChoiceIncapability,
|
|
40
|
+
resolveToolChoice,
|
|
41
|
+
} from "../utils/tool-choice-capability";
|
|
36
42
|
import { resolveAwsCredentials } from "./aws-credentials";
|
|
37
43
|
import { decodeEventStream } from "./aws-eventstream";
|
|
38
44
|
import { signRequest } from "./aws-sigv4";
|
|
@@ -43,7 +49,7 @@ export type BedrockThinkingDisplay = "summarized" | "omitted";
|
|
|
43
49
|
export interface BedrockOptions extends StreamOptions {
|
|
44
50
|
region?: string;
|
|
45
51
|
profile?: string;
|
|
46
|
-
toolChoice?:
|
|
52
|
+
toolChoice?: ToolChoice;
|
|
47
53
|
/* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */
|
|
48
54
|
reasoning?: Effort;
|
|
49
55
|
/* Custom token budgets per thinking level. Overrides default budgets. */
|
|
@@ -191,11 +197,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
191
197
|
|
|
192
198
|
try {
|
|
193
199
|
const cacheRetention = resolveCacheRetention(options.cacheRetention);
|
|
194
|
-
const
|
|
200
|
+
const resolvedToolChoice = resolveToolChoice(model, options.toolChoice);
|
|
201
|
+
const toolConfig = convertToolConfig(context.tools, resolvedToolChoice.resolvedChoice);
|
|
195
202
|
let additionalModelRequestFields = buildAdditionalModelRequestFields(model, options);
|
|
196
203
|
|
|
197
204
|
// Bedrock rejects thinking + forced tool_choice ("any" or specific tool).
|
|
198
|
-
// When tool_choice forces tool use, disable thinking to avoid API errors.
|
|
205
|
+
// When the resolved tool_choice forces tool use, disable thinking to avoid API errors.
|
|
199
206
|
if (toolConfig?.toolChoice && additionalModelRequestFields) {
|
|
200
207
|
const tc = toolConfig.toolChoice;
|
|
201
208
|
if (tc.any || tc.tool) additionalModelRequestFields = undefined;
|
|
@@ -254,8 +261,45 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
254
261
|
headers: baseHeaders,
|
|
255
262
|
});
|
|
256
263
|
const requestHeaders: Record<string, string> = { ...baseHeaders, ...signed };
|
|
264
|
+
const sentForcedToolChoice = Boolean(toolConfig?.toolChoice?.any || toolConfig?.toolChoice?.tool);
|
|
265
|
+
let fallbackRan = false;
|
|
266
|
+
const retryWithoutForcedToolChoice = async (reason: string) => {
|
|
267
|
+
fallbackRan = true;
|
|
268
|
+
markToolChoiceIncapability(model, "auto", reason);
|
|
269
|
+
stream.push({
|
|
270
|
+
type: "toolChoiceIncapability",
|
|
271
|
+
api: output.api,
|
|
272
|
+
provider: model.provider,
|
|
273
|
+
model: model.id,
|
|
274
|
+
requestedLevel: resolvedToolChoice.requestedLevel,
|
|
275
|
+
resolvedLevel: "auto",
|
|
276
|
+
reason,
|
|
277
|
+
registryKey: resolvedToolChoice.registryKey,
|
|
278
|
+
});
|
|
279
|
+
stripBedrockForcedToolChoiceForRetry(commandInput);
|
|
280
|
+
const retryBodyText = JSON.stringify(commandInput);
|
|
281
|
+
const retryBody = new TextEncoder().encode(retryBodyText);
|
|
282
|
+
const retrySigned = await signRequest({
|
|
283
|
+
method: "POST",
|
|
284
|
+
host,
|
|
285
|
+
path: urlPath,
|
|
286
|
+
body: retryBody,
|
|
287
|
+
region,
|
|
288
|
+
service: "bedrock",
|
|
289
|
+
credentials,
|
|
290
|
+
headers: baseHeaders,
|
|
291
|
+
});
|
|
292
|
+
if (rawRequestDump) rawRequestDump.body = commandInput;
|
|
293
|
+
return fetchWithRetry(url, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { ...baseHeaders, ...retrySigned },
|
|
296
|
+
body: retryBody,
|
|
297
|
+
signal: options.signal,
|
|
298
|
+
maxAttempts: 1,
|
|
299
|
+
});
|
|
300
|
+
};
|
|
257
301
|
|
|
258
|
-
|
|
302
|
+
let response = await fetchWithRetry(url, {
|
|
259
303
|
method: "POST",
|
|
260
304
|
headers: requestHeaders,
|
|
261
305
|
body,
|
|
@@ -263,6 +307,18 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
263
307
|
maxAttempts: resolveRetryBudget(options.requestMaxRetries, 4) + 1,
|
|
264
308
|
});
|
|
265
309
|
|
|
310
|
+
if (!response.ok && sentForcedToolChoice) {
|
|
311
|
+
const errBody = await response.text().catch(() => "");
|
|
312
|
+
const error = withHttpStatus(
|
|
313
|
+
new Error(`Bedrock HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
|
|
314
|
+
response.status,
|
|
315
|
+
);
|
|
316
|
+
if (firstTokenTime === undefined && !fallbackRan && isForcedToolChoiceUnsupportedError(error, true)) {
|
|
317
|
+
response = await retryWithoutForcedToolChoice(error.message);
|
|
318
|
+
} else {
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
266
322
|
if (!response.ok) {
|
|
267
323
|
const errBody = await response.text().catch(() => "");
|
|
268
324
|
throw withHttpStatus(
|
|
@@ -273,65 +329,86 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
273
329
|
if (!response.body) throw new Error("Bedrock response has no body");
|
|
274
330
|
|
|
275
331
|
// Track first event for the abort/diagnostic path (currently informational).
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
332
|
+
streamLoop: while (true) {
|
|
333
|
+
for await (const message of decodeEventStream(response.body)) {
|
|
334
|
+
const messageType = message.headers[":message-type"];
|
|
335
|
+
const eventType = message.headers[":event-type"];
|
|
336
|
+
|
|
337
|
+
if (messageType === "exception") {
|
|
338
|
+
const exceptionType = message.headers[":exception-type"] || "Exception";
|
|
339
|
+
const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
|
|
340
|
+
const errorMessage = payload?.message || new TextDecoder().decode(message.payload);
|
|
341
|
+
const status = exceptionType === "validationException" ? 400 : 0;
|
|
342
|
+
const err = new Error(`${exceptionType}: ${errorMessage}`);
|
|
343
|
+
const error = status ? withHttpStatus(err, status) : err;
|
|
344
|
+
if (
|
|
345
|
+
firstTokenTime === undefined &&
|
|
346
|
+
sentForcedToolChoice &&
|
|
347
|
+
!fallbackRan &&
|
|
348
|
+
isForcedToolChoiceUnsupportedError(error, true)
|
|
349
|
+
) {
|
|
350
|
+
response = await retryWithoutForcedToolChoice(error.message);
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
const errBody = await response.text().catch(() => "");
|
|
353
|
+
throw withHttpStatus(
|
|
354
|
+
new Error(`Bedrock HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
|
|
355
|
+
response.status,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (!response.body) throw new Error("Bedrock response has no body");
|
|
359
|
+
continue streamLoop;
|
|
304
360
|
}
|
|
305
|
-
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
case "contentBlockStart": {
|
|
309
|
-
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
310
|
-
handleContentBlockStart(payload as ContentBlockStartEvent, blocks, output, stream);
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
case "contentBlockDelta": {
|
|
314
|
-
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
315
|
-
handleContentBlockDelta(payload as ContentBlockDeltaEvent, blocks, output, stream);
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
case "contentBlockStop": {
|
|
319
|
-
handleContentBlockStop(payload as ContentBlockStopEvent, blocks, output, stream);
|
|
320
|
-
break;
|
|
361
|
+
throw error;
|
|
321
362
|
}
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
363
|
+
if (messageType === "error") {
|
|
364
|
+
const code = message.headers[":error-code"] || "UnknownError";
|
|
365
|
+
const errorMessage = message.headers[":error-message"] || new TextDecoder().decode(message.payload);
|
|
366
|
+
throw new Error(`${code}: ${errorMessage}`);
|
|
326
367
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
368
|
+
if (messageType !== "event") continue;
|
|
369
|
+
|
|
370
|
+
const payload = safeParsePayload(message.payload);
|
|
371
|
+
if (!payload) continue;
|
|
372
|
+
|
|
373
|
+
switch (eventType) {
|
|
374
|
+
case "messageStart": {
|
|
375
|
+
// no-op: first event marker is implicit by stream entry.
|
|
376
|
+
const ev = payload as MessageStartEvent;
|
|
377
|
+
if (ev.role !== "assistant") {
|
|
378
|
+
throw new Error("Unexpected assistant message start but got user message start instead");
|
|
379
|
+
}
|
|
380
|
+
stream.push({ type: "start", partial: output });
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "contentBlockStart": {
|
|
384
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
385
|
+
handleContentBlockStart(payload as ContentBlockStartEvent, blocks, output, stream);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case "contentBlockDelta": {
|
|
389
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
390
|
+
handleContentBlockDelta(payload as ContentBlockDeltaEvent, blocks, output, stream);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case "contentBlockStop": {
|
|
394
|
+
handleContentBlockStop(payload as ContentBlockStopEvent, blocks, output, stream);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "messageStop": {
|
|
398
|
+
const ev = payload as MessageStopEvent;
|
|
399
|
+
output.stopReason = mapStopReason(ev.stopReason);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "metadata": {
|
|
403
|
+
handleMetadata(payload as MetadataEvent, model, output);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
default:
|
|
407
|
+
// Unknown event types (Bedrock may add new ones) — ignore.
|
|
408
|
+
break;
|
|
330
409
|
}
|
|
331
|
-
default:
|
|
332
|
-
// Unknown event types (Bedrock may add new ones) — ignore.
|
|
333
|
-
break;
|
|
334
410
|
}
|
|
411
|
+
break;
|
|
335
412
|
}
|
|
336
413
|
|
|
337
414
|
if (options.signal?.aborted) throw new Error("Request was aborted");
|
|
@@ -714,7 +791,14 @@ function convertMessages(
|
|
|
714
791
|
return result;
|
|
715
792
|
}
|
|
716
793
|
|
|
717
|
-
function
|
|
794
|
+
export function stripBedrockForcedToolChoiceForRetry<T extends { toolConfig?: { toolChoice?: unknown } }>(body: T): T {
|
|
795
|
+
if (body.toolConfig) {
|
|
796
|
+
body.toolConfig = { ...body.toolConfig, toolChoice: undefined };
|
|
797
|
+
}
|
|
798
|
+
return body;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function convertToolConfig(
|
|
718
802
|
tools: Tool[] | undefined,
|
|
719
803
|
toolChoice: BedrockOptions["toolChoice"],
|
|
720
804
|
): WireToolConfig | undefined {
|
|
@@ -734,6 +818,7 @@ function convertToolConfig(
|
|
|
734
818
|
bedrockToolChoice = { auto: {} };
|
|
735
819
|
break;
|
|
736
820
|
case "any":
|
|
821
|
+
case "required":
|
|
737
822
|
bedrockToolChoice = { any: {} };
|
|
738
823
|
break;
|
|
739
824
|
default:
|
|
@@ -742,7 +827,7 @@ function convertToolConfig(
|
|
|
742
827
|
}
|
|
743
828
|
}
|
|
744
829
|
|
|
745
|
-
return { tools: bedrockTools, toolChoice: bedrockToolChoice };
|
|
830
|
+
return bedrockToolChoice ? { tools: bedrockTools, toolChoice: bedrockToolChoice } : { tools: bedrockTools };
|
|
746
831
|
}
|
|
747
832
|
|
|
748
833
|
function mapStopReason(reason: string | undefined): StopReason {
|
|
@@ -65,6 +65,12 @@ import { resolveRetryBudget } from "../utils/retry-budget";
|
|
|
65
65
|
import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
66
66
|
import { spillToDescription } from "../utils/schema/spill";
|
|
67
67
|
import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
68
|
+
import {
|
|
69
|
+
isForcedToolChoiceUnsupportedError,
|
|
70
|
+
markToolChoiceIncapability,
|
|
71
|
+
type ResolveToolChoiceResult,
|
|
72
|
+
resolveToolChoice,
|
|
73
|
+
} from "../utils/tool-choice-capability";
|
|
68
74
|
import {
|
|
69
75
|
buildCopilotDynamicHeaders,
|
|
70
76
|
hasCopilotVisionInput,
|
|
@@ -875,7 +881,8 @@ async function getAnthropicStreamResponse(
|
|
|
875
881
|
|
|
876
882
|
function getAnthropicCompat(
|
|
877
883
|
model: Model<"anthropic-messages">,
|
|
878
|
-
): Required<NonNullable<Model<"anthropic-messages">["compat"]>>
|
|
884
|
+
): Required<Omit<NonNullable<Model<"anthropic-messages">["compat"]>, "toolChoiceSupport">> &
|
|
885
|
+
Pick<NonNullable<Model<"anthropic-messages">["compat"]>, "toolChoiceSupport"> {
|
|
879
886
|
return {
|
|
880
887
|
disableStrictTools: model.compat?.disableStrictTools ?? false,
|
|
881
888
|
disableAdaptiveThinking: model.compat?.disableAdaptiveThinking ?? false,
|
|
@@ -883,6 +890,7 @@ function getAnthropicCompat(
|
|
|
883
890
|
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
|
|
884
891
|
supportsToolChoice: model.compat?.supportsToolChoice ?? true,
|
|
885
892
|
supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
|
|
893
|
+
toolChoiceSupport: model.compat?.toolChoiceSupport,
|
|
886
894
|
};
|
|
887
895
|
}
|
|
888
896
|
|
|
@@ -1076,8 +1084,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1076
1084
|
(providerSessionState?.strictToolsDisabled ?? false) || (model.compat?.disableStrictTools ?? false);
|
|
1077
1085
|
let strictFallbackErrorMessage: string | undefined;
|
|
1078
1086
|
let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
|
|
1087
|
+
let droppedForcedToolChoice = false;
|
|
1079
1088
|
const prepareParams = async (paramsOptions?: {
|
|
1080
1089
|
repairLatestAssistantThinking?: boolean;
|
|
1090
|
+
dropForcedToolChoice?: boolean;
|
|
1081
1091
|
}): Promise<MessageCreateParamsStreaming> => {
|
|
1082
1092
|
let nextParams = buildParams(
|
|
1083
1093
|
model,
|
|
@@ -1088,6 +1098,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1088
1098
|
disableStrictTools,
|
|
1089
1099
|
paramsOptions?.repairLatestAssistantThinking === true,
|
|
1090
1100
|
);
|
|
1101
|
+
if (paramsOptions?.dropForcedToolChoice === true) {
|
|
1102
|
+
delete nextParams.tool_choice;
|
|
1103
|
+
}
|
|
1091
1104
|
if (disableStrictTools) {
|
|
1092
1105
|
dropAnthropicStrictTools(nextParams);
|
|
1093
1106
|
}
|
|
@@ -1401,6 +1414,39 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1401
1414
|
firstTokenTime = undefined;
|
|
1402
1415
|
continue;
|
|
1403
1416
|
}
|
|
1417
|
+
if (
|
|
1418
|
+
!droppedForcedToolChoice &&
|
|
1419
|
+
firstTokenTime === undefined &&
|
|
1420
|
+
isSentForcedAnthropicToolChoice(params.tool_choice) &&
|
|
1421
|
+
isForcedToolChoiceUnsupportedError(streamFailure, true)
|
|
1422
|
+
) {
|
|
1423
|
+
const message = await finalizeErrorMessage(streamFailure, rawRequestDump);
|
|
1424
|
+
logger.debug("anthropic: forced tool_choice unsupported, retrying with auto tool choice", {
|
|
1425
|
+
model: model.id,
|
|
1426
|
+
error: message,
|
|
1427
|
+
});
|
|
1428
|
+
markToolChoiceIncapability(model, "auto", message);
|
|
1429
|
+
stream.push({
|
|
1430
|
+
type: "toolChoiceIncapability",
|
|
1431
|
+
api: output.api,
|
|
1432
|
+
provider: model.provider,
|
|
1433
|
+
model: model.id,
|
|
1434
|
+
requestedLevel: resolveToolChoice(model, options?.toolChoice).requestedLevel,
|
|
1435
|
+
resolvedLevel: "auto",
|
|
1436
|
+
reason: message,
|
|
1437
|
+
registryKey: resolveToolChoice(model, options?.toolChoice).registryKey,
|
|
1438
|
+
});
|
|
1439
|
+
droppedForcedToolChoice = true;
|
|
1440
|
+
params = await prepareParams({ dropForcedToolChoice: true });
|
|
1441
|
+
providerRetryAttempt = 0;
|
|
1442
|
+
output.content.length = 0;
|
|
1443
|
+
output.responseId = undefined;
|
|
1444
|
+
output.providerPayload = undefined;
|
|
1445
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
1446
|
+
output.stopReason = "stop";
|
|
1447
|
+
firstTokenTime = undefined;
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1404
1450
|
if (
|
|
1405
1451
|
!thinkingRepairAttempted &&
|
|
1406
1452
|
firstTokenTime === undefined &&
|
|
@@ -1684,28 +1730,29 @@ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming)
|
|
|
1684
1730
|
}
|
|
1685
1731
|
}
|
|
1686
1732
|
|
|
1687
|
-
function
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1733
|
+
function mapAnthropicToolChoice(
|
|
1734
|
+
toolChoice: NonNullable<ResolveToolChoiceResult["resolvedChoice"]>,
|
|
1735
|
+
isOAuthToken: boolean,
|
|
1736
|
+
): NonNullable<MessageCreateParamsStreaming["tool_choice"]> | undefined {
|
|
1737
|
+
if (typeof toolChoice === "string") {
|
|
1738
|
+
if (toolChoice === "required") return { type: "any" };
|
|
1739
|
+
return { type: toolChoice };
|
|
1740
|
+
}
|
|
1741
|
+
if ("function" in toolChoice) {
|
|
1742
|
+
const name = typeof toolChoice.function === "string" ? toolChoice.function : toolChoice.function.name;
|
|
1743
|
+
return { type: "tool", name: isOAuthToken ? applyClaudeToolPrefix(name) : name };
|
|
1744
|
+
}
|
|
1745
|
+
if ("name" in toolChoice && typeof toolChoice.name === "string") {
|
|
1746
|
+
return {
|
|
1747
|
+
...toolChoice,
|
|
1748
|
+
type: "tool",
|
|
1749
|
+
name: isOAuthToken ? applyClaudeToolPrefix(toolChoice.name) : toolChoice.name,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
return toolChoice as NonNullable<MessageCreateParamsStreaming["tool_choice"]>;
|
|
1701
1753
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
model: Model<"anthropic-messages">,
|
|
1705
|
-
toolChoice: NonNullable<AnthropicOptions["toolChoice"]>,
|
|
1706
|
-
): boolean {
|
|
1707
|
-
if (model.compat?.supportsToolChoice === false) return false;
|
|
1708
|
-
return !isForcedAnthropicToolChoice(toolChoice) || supportsForcedAnthropicToolChoice(model);
|
|
1754
|
+
function isSentForcedAnthropicToolChoice(toolChoice: MessageCreateParamsStreaming["tool_choice"] | undefined): boolean {
|
|
1755
|
+
return toolChoice?.type === "any" || toolChoice?.type === "tool";
|
|
1709
1756
|
}
|
|
1710
1757
|
|
|
1711
1758
|
function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
|
|
@@ -2055,16 +2102,22 @@ function buildParams(
|
|
|
2055
2102
|
(params as ParamsWithSpeed).speed = "fast";
|
|
2056
2103
|
}
|
|
2057
2104
|
|
|
2058
|
-
if (options?.toolChoice
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2105
|
+
if (options?.toolChoice) {
|
|
2106
|
+
const resolution = resolveToolChoice(model, options.toolChoice);
|
|
2107
|
+
if (resolution.degraded && resolution.supportSource !== "runtime") {
|
|
2108
|
+
logger.debug("anthropic: degrading tool_choice for model capability", {
|
|
2109
|
+
model: model.id,
|
|
2110
|
+
requestedLevel: resolution.requestedLevel,
|
|
2111
|
+
resolvedLevel: resolution.resolvedLevel,
|
|
2112
|
+
reason: resolution.reason,
|
|
2113
|
+
supportSource: resolution.supportSource,
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
if (resolution.resolvedChoice) {
|
|
2117
|
+
const mappedToolChoice = mapAnthropicToolChoice(resolution.resolvedChoice, isOAuthToken);
|
|
2118
|
+
if (mappedToolChoice) {
|
|
2119
|
+
params.tool_choice = mappedToolChoice;
|
|
2120
|
+
}
|
|
2068
2121
|
}
|
|
2069
2122
|
}
|
|
2070
2123
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, extractHttpStatusFromError } from "@gajae-code/utils";
|
|
1
|
+
import { $env, extractHttpStatusFromError, logger } from "@gajae-code/utils";
|
|
2
2
|
import { AzureOpenAI } from "openai";
|
|
3
3
|
import type {
|
|
4
4
|
Tool as OpenAITool,
|
|
@@ -30,6 +30,11 @@ import { resolveRetryBudget } from "../utils/retry-budget";
|
|
|
30
30
|
import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
31
31
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
32
32
|
import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
33
|
+
import {
|
|
34
|
+
isForcedToolChoiceUnsupportedError,
|
|
35
|
+
markToolChoiceIncapability,
|
|
36
|
+
resolveToolChoice,
|
|
37
|
+
} from "../utils/tool-choice-capability";
|
|
33
38
|
import { normalizeOpenAIResponsesPromptCacheKey, supportsDeveloperRole } from "./openai-responses";
|
|
34
39
|
import {
|
|
35
40
|
appendResponsesToolResultMessages,
|
|
@@ -130,7 +135,30 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
130
135
|
url: `${baseUrl}/responses`,
|
|
131
136
|
body: params,
|
|
132
137
|
};
|
|
133
|
-
|
|
138
|
+
let openaiStream: Awaited<ReturnType<typeof client.responses.create>>;
|
|
139
|
+
try {
|
|
140
|
+
openaiStream = await client.responses.create(params, { signal: requestSignal });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (!isForcedToolChoiceUnsupportedError(error, isForcedAzureResponsesToolChoice(params.tool_choice))) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
const reason = await finalizeErrorMessage(error, rawRequestDump);
|
|
146
|
+
markToolChoiceIncapability(model, "auto", reason);
|
|
147
|
+
const resolvedToolChoice = resolveToolChoice(model, options?.toolChoice);
|
|
148
|
+
stream.push({
|
|
149
|
+
type: "toolChoiceIncapability",
|
|
150
|
+
api: model.api,
|
|
151
|
+
provider: model.provider,
|
|
152
|
+
model: model.id,
|
|
153
|
+
requestedLevel: resolvedToolChoice.requestedLevel,
|
|
154
|
+
resolvedLevel: "auto",
|
|
155
|
+
reason,
|
|
156
|
+
registryKey: resolvedToolChoice.registryKey,
|
|
157
|
+
});
|
|
158
|
+
delete params.tool_choice;
|
|
159
|
+
rawRequestDump = { ...rawRequestDump, body: params };
|
|
160
|
+
openaiStream = await client.responses.create(params, { signal: requestSignal });
|
|
161
|
+
}
|
|
134
162
|
const firstEventWatchdog = createWatchdog(
|
|
135
163
|
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs),
|
|
136
164
|
() => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
@@ -278,7 +306,16 @@ function buildParams(
|
|
|
278
306
|
if (context.tools) {
|
|
279
307
|
params.tools = convertTools(context.tools);
|
|
280
308
|
if (options?.toolChoice) {
|
|
281
|
-
|
|
309
|
+
const toolChoice = resolveToolChoice(model, options.toolChoice);
|
|
310
|
+
if (toolChoice.degraded && toolChoice.supportSource === "runtime") {
|
|
311
|
+
logger.debug("azure-openai-responses: degraded tool_choice after runtime capability discovery", {
|
|
312
|
+
model: model.id,
|
|
313
|
+
requestedLevel: toolChoice.requestedLevel,
|
|
314
|
+
resolvedLevel: toolChoice.resolvedLevel,
|
|
315
|
+
reason: toolChoice.reason,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
params.tool_choice = mapToOpenAIResponsesToolChoice(toolChoice.resolvedChoice);
|
|
282
319
|
}
|
|
283
320
|
}
|
|
284
321
|
|
|
@@ -287,6 +324,10 @@ function buildParams(
|
|
|
287
324
|
return params;
|
|
288
325
|
}
|
|
289
326
|
|
|
327
|
+
function isForcedAzureResponsesToolChoice(choice: AzureOpenAIResponsesSamplingParams["tool_choice"]): boolean {
|
|
328
|
+
return !!choice && choice !== "none" && choice !== "auto";
|
|
329
|
+
}
|
|
330
|
+
|
|
290
331
|
function convertMessages(
|
|
291
332
|
model: Model<"azure-openai-responses">,
|
|
292
333
|
context: Context,
|