@gajae-code/ai 0.5.4 → 0.6.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 +14 -0
- package/dist/types/providers/composer-discipline.d.ts +2 -3
- package/dist/types/utils/schema/index.d.ts +1 -0
- package/dist/types/utils/schema/root-combinator.d.ts +12 -0
- package/package.json +2 -2
- package/src/model-thinking.ts +15 -0
- package/src/models.json +5 -5
- package/src/providers/amazon-bedrock.ts +2 -2
- package/src/providers/anthropic.ts +17 -25
- package/src/providers/azure-openai-responses.ts +2 -2
- package/src/providers/composer-discipline.ts +10 -7
- package/src/providers/cursor.ts +8 -8
- package/src/providers/ollama.ts +2 -2
- package/src/providers/openai-codex-responses.ts +8 -2
- package/src/providers/openai-completions.ts +4 -4
- package/src/providers/openai-responses.ts +12 -2
- package/src/utils/schema/index.ts +1 -0
- package/src/utils/schema/root-combinator.ts +143 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.1] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Generalized tool `input_schema` root-combinator flattening across providers so discriminated-union tool inputs (e.g. the `computer` tool, a `z.union`) no longer ship a bare top-level `anyOf`/`oneOf`/`allOf` root that strict validators reject. The Anthropic-only fix from 0.5.4 is now the shared, provider-agnostic `flattenToolRootCombinators` (in `utils/schema`) and is applied by Amazon Bedrock, OpenAI Chat Completions / Responses / Codex-Responses / Azure-Responses, Ollama, and Cursor. Previously only Anthropic flattened the root, so those providers forwarded the union root verbatim and a union-root tool failed upstream — Bedrock Converse (including via Kiro/CodeWhisperer relays) returned `400 TOOL_SCHEMA_INVALID: The value at toolConfig.tools.N.toolSpec.inputSchema.json.type must be one of the following: object`. Anthropic behavior is unchanged (it now calls the shared util), Google / Cloud Code Assist keep their own object-merge, and object-root tools, nested combinators, and runtime Zod validation are all untouched.
|
|
10
|
+
|
|
11
|
+
## [0.6.0] - 2026-06-18
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Corrected the bundled `zai/glm-5.2` context window from 200K to its true 1M (1,000,000-token) lossless window. GLM-5.2 was added in #579 by copying GLM-5.1's 200K entry, and that stale value survived every `generate-models` run because provider-scoped models bypass the models.dev refresh in `applyGlobalModelsDevFallback`. Added a regen-safe pin in `applyGeneratedModelPolicy` (mirroring the Bedrock-Opus-4.6 precedent) so the 1M value persists, and updated the bundled catalog entry. The wrong 200K tripped auto-compaction / context-cap thresholds ~5x early for GLM-5.2 sessions.
|
|
15
|
+
- Corrected the bundled `minimax-m3` context window from 512K to its true 1M (1,000,000-token) window per the official MiniMax docs (platform.minimax.io documents MiniMax-M3 as a 1M-context frontier coding model). All four provider copies (`minimax`, `minimax-cn`, `minimax-code`, `minimax-code-cn`) carried the stale 512K, which survived `generate-models` (provider-scoped models bypass the models.dev refresh in `applyGlobalModelsDevFallback`) and tripped auto-compaction / context-cap thresholds 2x early on MiniMax sessions. Added a regen-safe pin in `applyGeneratedModelPolicy` keyed on `model.id === "minimax-m3"` and updated the bundled entries. `minimax-v3` (an undocumented catalog alias) is intentionally left untouched.
|
|
16
|
+
|
|
5
17
|
## [0.5.4] - 2026-06-17
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
8
20
|
|
|
21
|
+
- Fixed Anthropic tool schema compatibility for discriminated-union tool inputs by flattening only the model-facing input-schema root, avoiding top-level `oneOf`/`anyOf`/`allOf` request rejections while preserving nested combinators and runtime validation authority.
|
|
22
|
+
|
|
9
23
|
- Made the "No API key for provider" error from `stream`/`complete` actionable for OpenCode Go/Zen subscription providers in headless runs (#755). The subscription is itself an API key (`OPENCODE_API_KEY`, created at https://opencode.ai/auth), not a separate OAuth/session token; the new `formatProviderCredentialHint` helper (composed into `formatMissingApiKeyError`) names the env var GJC reads, warns that a project `.env` is intentionally ignored for provider credentials, and points OpenCode users at the one-time interactive `gjc auth-broker login <provider>` credential capture to run before headless/print mode. No auth behavior changed.
|
|
10
24
|
|
|
11
25
|
## [0.5.3] - 2026-06-16
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
* syntax errors.
|
|
21
21
|
*
|
|
22
22
|
* This prompt is the per-request countermeasure, pinned ahead of the host
|
|
23
|
-
* system prompt on
|
|
23
|
+
* system prompt on openai-completions, openai-responses, and cursor RPC paths.
|
|
24
24
|
*/
|
|
25
|
-
/** Matches composer-harness model ids on any provider (xai grok-composer-*, cursor composer-*). */
|
|
26
25
|
export declare function isComposerHarnessModel(modelId: string): boolean;
|
|
27
|
-
export declare const COMPOSER_EDIT_DISCIPLINE_PROMPT = "File-editing discipline for this harness (this OVERRIDES contrary habits from your training):\n\n-
|
|
26
|
+
export declare const COMPOSER_EDIT_DISCIPLINE_PROMPT = "File-editing discipline for this Composer harness (this OVERRIDES contrary habits from your training):\n\n- Discover file names ONLY with the find tool; search file contents ONLY with the search tool; read file bodies or line ranges ONLY with the read tool. NEVER inspect repository files through shell commands (ls, find, fd, cat, sed, awk, grep, rg, head, tail, less, more) or scripts \u2014 that output carries no hashline anchors and bypasses the agent's safety limits.\n- Modify files ONLY with the edit/write tools. NEVER mutate files through shell redirection, tee, sed -i, perl -pi, inline python/node/bun scripts, or other out-of-band writes \u2014 those writes invalidate every known anchor and break edit recovery.\n- A line anchor (e.g. \"42sr\") is a line number plus a 2-char content hash. You CANNOT compute the hash yourself: copy anchors verbatim from the MOST RECENT read/search/edit output of that exact file. NEVER guess, renumber, or arithmetically shift an anchor.\n- After ANY edit to a file (including your own), anchors you saw earlier are stale. Re-read the edited region, or copy the fresh anchors printed in the edit result, before issuing the next edit.\n- If an edit is rejected with \"anchors do not match\", the rejection message prints the current lines WITH fresh anchors. Retry using exactly those printed anchors.\n- Tool-call arguments must be the exact JSON/schema object requested by the tool. Do not include Markdown, commentary, analysis text, or invented fields inside tool arguments.\n- Use bash only for terminal operations such as tests, builds, package scripts, and git commands. A shell command string must contain only the command itself; NEVER interleave reasoning or commentary into command strings or heredocs.";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** True when a JSON Schema node describes an object (explicit `type` or `properties`). */
|
|
2
|
+
export declare function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Flatten a provider-emitted tool ROOT whose top level is a `oneOf`/`anyOf`/`allOf`
|
|
5
|
+
* combinator into one `type: "object"` schema: merge object-branch properties,
|
|
6
|
+
* derive the discriminant (`action`) enum, keep the common required set, and demote
|
|
7
|
+
* leftover combinators plus per-branch guidance into the description. Nested
|
|
8
|
+
* combinators (inside individual properties) are left untouched.
|
|
9
|
+
*
|
|
10
|
+
* Idempotent: a root that already lacks top-level combinators is returned unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export declare function flattenToolRootCombinators(schema: Record<string, unknown>): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.
|
|
46
|
+
"@gajae-code/utils": "0.6.1",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
package/src/model-thinking.ts
CHANGED
|
@@ -397,6 +397,21 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
|
|
|
397
397
|
if (parsedModel.family === "openai") {
|
|
398
398
|
applyOpenAICatalogPolicy(model, parsedModel);
|
|
399
399
|
}
|
|
400
|
+
// GLM-5.2 (Zhipu/ZAI): ships a 1M lossless context window, but the bundled
|
|
401
|
+
// catalog copied GLM-5.1's 200K and that stale value survives generate-models
|
|
402
|
+
// (provider-scoped models bypass the models.dev refresh in applyGlobalModelsDevFallback).
|
|
403
|
+
// Pin to the true 1M so context-cap / auto-compaction thresholds aren't tripped ~5x early.
|
|
404
|
+
if (model.provider === "zai" && model.id === "glm-5.2") {
|
|
405
|
+
model.contextWindow = 1_000_000;
|
|
406
|
+
}
|
|
407
|
+
// MiniMax-M3: official MiniMax docs (platform.minimax.io/docs/guides/models-intro)
|
|
408
|
+
// document a 1M context window, but models.dev and the bundled catalog both report
|
|
409
|
+
// 512K. The stale 512K survives generate-models (provider-scoped models bypass the
|
|
410
|
+
// models.dev refresh in applyGlobalModelsDevFallback), tripping auto-compaction /
|
|
411
|
+
// context-cap thresholds 2x early on MiniMax sessions. Pin to the true 1M.
|
|
412
|
+
if (model.id === "minimax-m3") {
|
|
413
|
+
model.contextWindow = 1_000_000;
|
|
414
|
+
}
|
|
400
415
|
}
|
|
401
416
|
|
|
402
417
|
function scrubGeneratedModelName(name: string): string {
|
package/src/models.json
CHANGED
|
@@ -34712,7 +34712,7 @@
|
|
|
34712
34712
|
"cacheRead": 0.12,
|
|
34713
34713
|
"cacheWrite": 0
|
|
34714
34714
|
},
|
|
34715
|
-
"contextWindow":
|
|
34715
|
+
"contextWindow": 1000000,
|
|
34716
34716
|
"maxTokens": 128000,
|
|
34717
34717
|
"thinking": {
|
|
34718
34718
|
"mode": "budget",
|
|
@@ -34907,7 +34907,7 @@
|
|
|
34907
34907
|
"cacheRead": 0.12,
|
|
34908
34908
|
"cacheWrite": 0
|
|
34909
34909
|
},
|
|
34910
|
-
"contextWindow":
|
|
34910
|
+
"contextWindow": 1000000,
|
|
34911
34911
|
"maxTokens": 128000,
|
|
34912
34912
|
"thinking": {
|
|
34913
34913
|
"mode": "budget",
|
|
@@ -35174,7 +35174,7 @@
|
|
|
35174
35174
|
"cacheRead": 0,
|
|
35175
35175
|
"cacheWrite": 0
|
|
35176
35176
|
},
|
|
35177
|
-
"contextWindow":
|
|
35177
|
+
"contextWindow": 1000000,
|
|
35178
35178
|
"maxTokens": 128000,
|
|
35179
35179
|
"compat": {
|
|
35180
35180
|
"supportsStore": false,
|
|
@@ -35478,7 +35478,7 @@
|
|
|
35478
35478
|
"cacheRead": 0,
|
|
35479
35479
|
"cacheWrite": 0
|
|
35480
35480
|
},
|
|
35481
|
-
"contextWindow":
|
|
35481
|
+
"contextWindow": 1000000,
|
|
35482
35482
|
"maxTokens": 128000,
|
|
35483
35483
|
"compat": {
|
|
35484
35484
|
"supportsStore": false,
|
|
@@ -71628,7 +71628,7 @@
|
|
|
71628
71628
|
"cacheRead": 0,
|
|
71629
71629
|
"cacheWrite": 0
|
|
71630
71630
|
},
|
|
71631
|
-
"contextWindow":
|
|
71631
|
+
"contextWindow": 1000000,
|
|
71632
71632
|
"maxTokens": 131072,
|
|
71633
71633
|
"thinking": {
|
|
71634
71634
|
"mode": "budget",
|
|
@@ -33,7 +33,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
33
33
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
34
34
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
35
35
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
36
|
-
import { toolWireSchema } from "../utils/schema
|
|
36
|
+
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
37
37
|
import {
|
|
38
38
|
isForcedToolChoiceUnsupportedError,
|
|
39
39
|
markToolChoiceIncapability,
|
|
@@ -808,7 +808,7 @@ export function convertToolConfig(
|
|
|
808
808
|
toolSpec: {
|
|
809
809
|
name: tool.name,
|
|
810
810
|
description: tool.description || "",
|
|
811
|
-
inputSchema: { json: toolWireSchema(tool) },
|
|
811
|
+
inputSchema: { json: flattenToolRootCombinators(toolWireSchema(tool)) },
|
|
812
812
|
},
|
|
813
813
|
}));
|
|
814
814
|
|
|
@@ -62,7 +62,13 @@ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
|
62
62
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
63
63
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
64
64
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
COMBINATOR_KEYS,
|
|
67
|
+
flattenToolRootCombinators,
|
|
68
|
+
isJsonSchemaObjectNode,
|
|
69
|
+
NO_STRICT,
|
|
70
|
+
toolWireSchema,
|
|
71
|
+
} from "../utils/schema";
|
|
66
72
|
import { spillToDescription } from "../utils/schema/spill";
|
|
67
73
|
import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
68
74
|
import {
|
|
@@ -2406,22 +2412,6 @@ const MAX_ANTHROPIC_STRICT_TOOLS = 20;
|
|
|
2406
2412
|
const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
|
|
2407
2413
|
const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
|
|
2408
2414
|
|
|
2409
|
-
/** `minItems` / `maxItems` apply to arrays; Anthropic rejects them on `type: "object"` (including `minItems: 0`/`1`). */
|
|
2410
|
-
function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
|
|
2411
|
-
const t = schema.type;
|
|
2412
|
-
if (t === "array") return true;
|
|
2413
|
-
if (Array.isArray(t) && t.includes("array") && !t.includes("object")) return true;
|
|
2414
|
-
return false;
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean {
|
|
2418
|
-
if (isJsonSchemaArrayNode(schema)) return false;
|
|
2419
|
-
if (schema.type === "object") return true;
|
|
2420
|
-
if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
|
|
2421
|
-
if (isRecord(schema.properties)) return true;
|
|
2422
|
-
return false;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
2415
|
/**
|
|
2426
2416
|
* Pick the principal non-null scalar type from a `type` keyword. Anthropic accepts
|
|
2427
2417
|
* `type` as either a single string or an array (e.g. `["number", "null"]` for a
|
|
@@ -2740,14 +2730,16 @@ function normalizeAnthropicStrictSchema(
|
|
|
2740
2730
|
|
|
2741
2731
|
function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
|
|
2742
2732
|
const jsonSchema = toolWireSchema(tool);
|
|
2743
|
-
return
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2733
|
+
return flattenToolRootCombinators(
|
|
2734
|
+
normalizeAnthropicToolSchema({
|
|
2735
|
+
...jsonSchema,
|
|
2736
|
+
type: "object",
|
|
2737
|
+
properties: isRecord(jsonSchema.properties) ? jsonSchema.properties : {},
|
|
2738
|
+
required: Array.isArray(jsonSchema.required)
|
|
2739
|
+
? jsonSchema.required.filter((entry): entry is string => typeof entry === "string")
|
|
2740
|
+
: [],
|
|
2741
|
+
}) as Record<string, unknown>,
|
|
2742
|
+
);
|
|
2751
2743
|
}
|
|
2752
2744
|
|
|
2753
2745
|
function buildAnthropicToolSchemaPlans(tools: Tool[], disableStrictTools = false): AnthropicToolSchemaPlan[] {
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
iterateWithIdleTimeout,
|
|
28
28
|
} from "../utils/idle-iterator";
|
|
29
29
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
30
|
-
import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
30
|
+
import { flattenToolRootCombinators, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
31
31
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
32
32
|
import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
33
33
|
import {
|
|
@@ -373,7 +373,7 @@ function convertTools(tools: Tool[]): OpenAITool[] {
|
|
|
373
373
|
type: "function",
|
|
374
374
|
name: tool.name,
|
|
375
375
|
description: tool.description || "",
|
|
376
|
-
parameters: sanitizeSchemaForOpenAIResponses(toolWireSchema(tool)),
|
|
376
|
+
parameters: sanitizeSchemaForOpenAIResponses(flattenToolRootCombinators(toolWireSchema(tool))),
|
|
377
377
|
strict: false,
|
|
378
378
|
}));
|
|
379
379
|
}
|
|
@@ -20,19 +20,22 @@
|
|
|
20
20
|
* syntax errors.
|
|
21
21
|
*
|
|
22
22
|
* This prompt is the per-request countermeasure, pinned ahead of the host
|
|
23
|
-
* system prompt on
|
|
23
|
+
* system prompt on openai-completions, openai-responses, and cursor RPC paths.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
/** Matches composer-harness model ids on any provider (xai grok-composer-*, cursor composer-*). */
|
|
26
|
+
/** Matches composer-harness model ids on any provider (xai grok-composer-*, cursor composer-* / composer2.*). */
|
|
27
|
+
const COMPOSER_MODEL_ID_PATTERN = /(?:^|[/:._-])(?:grok-)?composer(?:[/:._-]|(?=\d)|$)/i;
|
|
28
|
+
|
|
27
29
|
export function isComposerHarnessModel(modelId: string): boolean {
|
|
28
|
-
return
|
|
30
|
+
return COMPOSER_MODEL_ID_PATTERN.test(modelId);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export const COMPOSER_EDIT_DISCIPLINE_PROMPT = `File-editing discipline for this harness (this OVERRIDES contrary habits from your training):
|
|
33
|
+
export const COMPOSER_EDIT_DISCIPLINE_PROMPT = `File-editing discipline for this Composer harness (this OVERRIDES contrary habits from your training):
|
|
32
34
|
|
|
33
|
-
-
|
|
34
|
-
- Modify files ONLY with the
|
|
35
|
+
- Discover file names ONLY with the find tool; search file contents ONLY with the search tool; read file bodies or line ranges ONLY with the read tool. NEVER inspect repository files through shell commands (ls, find, fd, cat, sed, awk, grep, rg, head, tail, less, more) or scripts — that output carries no hashline anchors and bypasses the agent's safety limits.
|
|
36
|
+
- Modify files ONLY with the edit/write tools. NEVER mutate files through shell redirection, tee, sed -i, perl -pi, inline python/node/bun scripts, or other out-of-band writes — those writes invalidate every known anchor and break edit recovery.
|
|
35
37
|
- A line anchor (e.g. "42sr") is a line number plus a 2-char content hash. You CANNOT compute the hash yourself: copy anchors verbatim from the MOST RECENT read/search/edit output of that exact file. NEVER guess, renumber, or arithmetically shift an anchor.
|
|
36
38
|
- After ANY edit to a file (including your own), anchors you saw earlier are stale. Re-read the edited region, or copy the fresh anchors printed in the edit result, before issuing the next edit.
|
|
37
39
|
- If an edit is rejected with "anchors do not match", the rejection message prints the current lines WITH fresh anchors. Retry using exactly those printed anchors.
|
|
38
|
-
-
|
|
40
|
+
- Tool-call arguments must be the exact JSON/schema object requested by the tool. Do not include Markdown, commentary, analysis text, or invented fields inside tool arguments.
|
|
41
|
+
- Use bash only for terminal operations such as tests, builds, package scripts, and git commands. A shell command string must contain only the command itself; NEVER interleave reasoning or commentary into command strings or heredocs.`;
|
package/src/providers/cursor.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { normalizeSystemPrompts } from "../utils";
|
|
|
29
29
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
30
30
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
31
31
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
32
|
-
import { toolWireSchema } from "../utils/schema
|
|
32
|
+
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
33
33
|
import { COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
|
|
34
34
|
import type { McpToolDefinition } from "./cursor/gen/agent_pb";
|
|
35
35
|
import {
|
|
@@ -2183,7 +2183,7 @@ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[]
|
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
2185
2185
|
return advertisedTools.map(tool => {
|
|
2186
|
-
const jsonSchema = toolWireSchema(tool);
|
|
2186
|
+
const jsonSchema = flattenToolRootCombinators(toolWireSchema(tool));
|
|
2187
2187
|
const schemaValue: JsonValue =
|
|
2188
2188
|
jsonSchema && typeof jsonSchema === "object"
|
|
2189
2189
|
? (jsonSchema as JsonValue)
|
|
@@ -2321,12 +2321,12 @@ function findLastUserMessageIndex(messages: Message[]): number {
|
|
|
2321
2321
|
*/
|
|
2322
2322
|
export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined, modelId?: string): string[] {
|
|
2323
2323
|
const systemPrompts = normalizeSystemPrompts(systemPrompt);
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
// Composer-harness models need anchor/edit discipline pinned ahead of
|
|
2329
|
-
// host prompt (see composer-discipline.ts for the observed failure modes).
|
|
2324
|
+
const jsons =
|
|
2325
|
+
systemPrompts.length === 0
|
|
2326
|
+
? [JSON.stringify({ role: "system", content: "You are a helpful assistant." })]
|
|
2327
|
+
: systemPrompts.map(content => JSON.stringify({ role: "system", content }));
|
|
2328
|
+
// Composer-harness models need anchor/edit discipline pinned ahead of any
|
|
2329
|
+
// host/default prompt (see composer-discipline.ts for the observed failure modes).
|
|
2330
2330
|
if (modelId !== undefined && isComposerHarnessModel(modelId)) {
|
|
2331
2331
|
jsons.unshift(JSON.stringify({ role: "system", content: COMPOSER_EDIT_DISCIPLINE_PROMPT }));
|
|
2332
2332
|
}
|
package/src/providers/ollama.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
19
19
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
20
20
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
21
21
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
22
|
-
import { toolWireSchema } from "../utils/schema
|
|
22
|
+
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
23
23
|
import {
|
|
24
24
|
isForcedToolChoiceUnsupportedError,
|
|
25
25
|
markToolChoiceIncapability,
|
|
@@ -253,7 +253,7 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
253
253
|
function: {
|
|
254
254
|
name: tool.name,
|
|
255
255
|
description: tool.description,
|
|
256
|
-
parameters: toolWireSchema(tool),
|
|
256
|
+
parameters: flattenToolRootCombinators(toolWireSchema(tool)),
|
|
257
257
|
},
|
|
258
258
|
}));
|
|
259
259
|
}
|
|
@@ -50,7 +50,13 @@ import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-ins
|
|
|
50
50
|
import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
51
51
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
52
52
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
adaptSchemaForStrict,
|
|
55
|
+
flattenToolRootCombinators,
|
|
56
|
+
NO_STRICT,
|
|
57
|
+
sanitizeSchemaForOpenAIResponses,
|
|
58
|
+
toolWireSchema,
|
|
59
|
+
} from "../utils/schema";
|
|
54
60
|
import {
|
|
55
61
|
isForcedToolChoiceUnsupportedError,
|
|
56
62
|
markToolChoiceIncapability,
|
|
@@ -2658,7 +2664,7 @@ export function convertOpenAICodexResponsesTools(
|
|
|
2658
2664
|
};
|
|
2659
2665
|
}
|
|
2660
2666
|
const strict = !!(!NO_STRICT && tool.strict);
|
|
2661
|
-
const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
|
|
2667
|
+
const baseParameters = sanitizeSchemaForOpenAIResponses(flattenToolRootCombinators(toolWireSchema(tool)));
|
|
2662
2668
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
|
|
2663
2669
|
return {
|
|
2664
2670
|
type: "function",
|
|
@@ -57,7 +57,7 @@ import { getKimiCommonHeaders } from "../utils/oauth/kimi";
|
|
|
57
57
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
58
58
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
59
59
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
60
|
-
import { adaptSchemaForStrict, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
60
|
+
import { adaptSchemaForStrict, flattenToolRootCombinators, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
61
61
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
62
62
|
import { type HealedToolCall, modelMayLeakKimiToolCalls, ToolCallHealer } from "../utils/tool-call-healing";
|
|
63
63
|
import { isForcedToolChoice, mapToOpenAICompletionsToolChoice } from "../utils/tool-choice";
|
|
@@ -1465,9 +1465,9 @@ export function convertMessages(
|
|
|
1465
1465
|
};
|
|
1466
1466
|
|
|
1467
1467
|
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
1468
|
-
// Composer-harness models need anchor/edit discipline pinned ahead of
|
|
1468
|
+
// Composer-harness models need anchor/edit discipline pinned ahead of any
|
|
1469
1469
|
// host prompt (see composer-discipline.ts for the observed failure modes).
|
|
1470
|
-
if (
|
|
1470
|
+
if (isComposerHarnessModel(model.id)) {
|
|
1471
1471
|
systemPrompts.unshift(COMPOSER_EDIT_DISCIPLINE_PROMPT);
|
|
1472
1472
|
}
|
|
1473
1473
|
if (systemPrompts.length > 0) {
|
|
@@ -1832,7 +1832,7 @@ function convertTools(
|
|
|
1832
1832
|
): BuiltOpenAICompletionTools {
|
|
1833
1833
|
const adaptedTools = tools.map(tool => {
|
|
1834
1834
|
const strict = !NO_STRICT && compat.supportsStrictMode !== false && tool.strict !== false;
|
|
1835
|
-
const baseParameters = toolWireSchema(tool);
|
|
1835
|
+
const baseParameters = flattenToolRootCombinators(toolWireSchema(tool));
|
|
1836
1836
|
const adapted = adaptSchemaForStrict(baseParameters, strict);
|
|
1837
1837
|
return {
|
|
1838
1838
|
tool,
|
|
@@ -50,7 +50,13 @@ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
|
50
50
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
51
51
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
52
52
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
adaptSchemaForStrict,
|
|
55
|
+
flattenToolRootCombinators,
|
|
56
|
+
NO_STRICT,
|
|
57
|
+
sanitizeSchemaForOpenAIResponses,
|
|
58
|
+
toolWireSchema,
|
|
59
|
+
} from "../utils/schema";
|
|
54
60
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
55
61
|
import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
56
62
|
import {
|
|
@@ -58,6 +64,7 @@ import {
|
|
|
58
64
|
markToolChoiceIncapability,
|
|
59
65
|
resolveToolChoice,
|
|
60
66
|
} from "../utils/tool-choice-capability";
|
|
67
|
+
import { COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
|
|
61
68
|
import {
|
|
62
69
|
buildCopilotDynamicHeaders,
|
|
63
70
|
hasCopilotVisionInput,
|
|
@@ -485,6 +492,9 @@ function buildParams(
|
|
|
485
492
|
const messages: ResponseInput = [...conversationMessages];
|
|
486
493
|
|
|
487
494
|
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
495
|
+
if (isComposerHarnessModel(model.id)) {
|
|
496
|
+
systemPrompts.unshift(COMPOSER_EDIT_DISCIPLINE_PROMPT);
|
|
497
|
+
}
|
|
488
498
|
let systemInstructions: string | undefined;
|
|
489
499
|
if (systemPrompts.length > 0) {
|
|
490
500
|
const needsDeveloperRole =
|
|
@@ -720,7 +730,7 @@ export function convertTools(tools: Tool[], strictMode: boolean, model: Model<"o
|
|
|
720
730
|
} as unknown as OpenAITool;
|
|
721
731
|
}
|
|
722
732
|
const strict = !NO_STRICT && strictMode && tool.strict !== false;
|
|
723
|
-
const baseParameters = toolWireSchema(tool);
|
|
733
|
+
const baseParameters = flattenToolRootCombinators(toolWireSchema(tool));
|
|
724
734
|
const responseParameters = sanitizeSchemaForOpenAIResponses(baseParameters);
|
|
725
735
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(responseParameters, strict);
|
|
726
736
|
return {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `input_schema` root flattening.
|
|
3
|
+
*
|
|
4
|
+
* Tool/function-calling `input_schema` roots MUST be a single JSON Schema object.
|
|
5
|
+
* Bedrock Converse (`toolConfig.tools[*].toolSpec.inputSchema.json.type must be
|
|
6
|
+
* one of the following: object`), OpenAI strict mode, Gemini, and Anthropic all
|
|
7
|
+
* reject roots whose top level is a bare `oneOf`/`anyOf`/`allOf` combinator (the
|
|
8
|
+
* validators require `type: "object"`).
|
|
9
|
+
*
|
|
10
|
+
* Zod `z.union(...)` / `z.discriminatedUnion(...)` tool parameters serialize to
|
|
11
|
+
* exactly such a combinator root, so this normalization is provider-agnostic and
|
|
12
|
+
* runs inside the shared wire pipeline (`toolWireSchema`).
|
|
13
|
+
*/
|
|
14
|
+
import { isRecord } from "@gajae-code/utils";
|
|
15
|
+
import { COMBINATOR_KEYS } from "./fields";
|
|
16
|
+
import { spillToDescription } from "./spill";
|
|
17
|
+
|
|
18
|
+
/** `minItems` / `maxItems` apply to arrays; some validators reject them on `type: "object"`. */
|
|
19
|
+
function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
|
|
20
|
+
const t = schema.type;
|
|
21
|
+
if (t === "array") return true;
|
|
22
|
+
if (Array.isArray(t) && t.includes("array") && !t.includes("object")) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when a JSON Schema node describes an object (explicit `type` or `properties`). */
|
|
27
|
+
export function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean {
|
|
28
|
+
if (isJsonSchemaArrayNode(schema)) return false;
|
|
29
|
+
if (schema.type === "object") return true;
|
|
30
|
+
if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
|
|
31
|
+
if (isRecord(schema.properties)) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRequiredNames(schema: Record<string, unknown>): Set<string> {
|
|
36
|
+
return new Set(
|
|
37
|
+
Array.isArray(schema.required)
|
|
38
|
+
? schema.required.filter((entry): entry is string => typeof entry === "string")
|
|
39
|
+
: [],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getSingleLiteralValue(schema: unknown): unknown | undefined {
|
|
44
|
+
if (!isRecord(schema)) return undefined;
|
|
45
|
+
if (Object.hasOwn(schema, "const")) return schema.const;
|
|
46
|
+
if (Array.isArray(schema.enum) && schema.enum.length === 1) return schema.enum[0];
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function describeRootBranch(index: number, branch: Record<string, unknown>, action: unknown): string {
|
|
51
|
+
const required = [...getRequiredNames(branch)];
|
|
52
|
+
const parts = [`Branch ${index + 1}`];
|
|
53
|
+
if (typeof action === "string" || typeof action === "number" || typeof action === "boolean") {
|
|
54
|
+
parts.push(`action ${JSON.stringify(action)}`);
|
|
55
|
+
}
|
|
56
|
+
if (required.length > 0) parts.push(`branch-required fields: ${required.join(", ")}`);
|
|
57
|
+
if (typeof branch.description === "string" && branch.description.length > 0) parts.push(branch.description);
|
|
58
|
+
return parts.join("; ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectRootObjectBranches(schema: unknown): Record<string, unknown>[] | undefined {
|
|
62
|
+
if (!isRecord(schema)) return undefined;
|
|
63
|
+
if (isJsonSchemaObjectNode(schema)) return [schema];
|
|
64
|
+
|
|
65
|
+
const combinatorKeys = COMBINATOR_KEYS.filter(key => Array.isArray(schema[key]));
|
|
66
|
+
if (combinatorKeys.length === 0) return undefined;
|
|
67
|
+
|
|
68
|
+
const branches: Record<string, unknown>[] = [];
|
|
69
|
+
for (const key of combinatorKeys) {
|
|
70
|
+
const variants = schema[key];
|
|
71
|
+
if (!Array.isArray(variants) || variants.length === 0) return undefined;
|
|
72
|
+
for (const variant of variants) {
|
|
73
|
+
const nestedBranches = collectRootObjectBranches(variant);
|
|
74
|
+
if (nestedBranches === undefined) return undefined;
|
|
75
|
+
branches.push(...nestedBranches);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return branches;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Flatten a provider-emitted tool ROOT whose top level is a `oneOf`/`anyOf`/`allOf`
|
|
83
|
+
* combinator into one `type: "object"` schema: merge object-branch properties,
|
|
84
|
+
* derive the discriminant (`action`) enum, keep the common required set, and demote
|
|
85
|
+
* leftover combinators plus per-branch guidance into the description. Nested
|
|
86
|
+
* combinators (inside individual properties) are left untouched.
|
|
87
|
+
*
|
|
88
|
+
* Idempotent: a root that already lacks top-level combinators is returned unchanged.
|
|
89
|
+
*/
|
|
90
|
+
export function flattenToolRootCombinators(schema: Record<string, unknown>): Record<string, unknown> {
|
|
91
|
+
const rootCombinators = COMBINATOR_KEYS.filter(key => Array.isArray(schema[key]));
|
|
92
|
+
if (rootCombinators.length === 0) return schema;
|
|
93
|
+
const result: Record<string, unknown> = { ...schema };
|
|
94
|
+
|
|
95
|
+
const baseProperties = isRecord(result.properties) ? { ...result.properties } : {};
|
|
96
|
+
const flattenedBranches = rootCombinators
|
|
97
|
+
.map(key => ({ key, branches: collectRootObjectBranches({ [key]: result[key] }) }))
|
|
98
|
+
.find(entry => entry.branches !== undefined && entry.branches.length > 0);
|
|
99
|
+
|
|
100
|
+
result.type = "object";
|
|
101
|
+
result.properties = baseProperties;
|
|
102
|
+
result.additionalProperties = result.additionalProperties === undefined ? false : result.additionalProperties;
|
|
103
|
+
|
|
104
|
+
if (flattenedBranches?.branches !== undefined) {
|
|
105
|
+
const variants = flattenedBranches.branches;
|
|
106
|
+
const commonRequired = variants.map(variant => getRequiredNames(variant));
|
|
107
|
+
const required = [...commonRequired[0]].filter(name => commonRequired.every(set => set.has(name)));
|
|
108
|
+
const actionValues: unknown[] = [];
|
|
109
|
+
const guidance: string[] = [];
|
|
110
|
+
|
|
111
|
+
for (const [index, branch] of variants.entries()) {
|
|
112
|
+
if (isRecord(branch.properties)) {
|
|
113
|
+
Object.assign(baseProperties, branch.properties);
|
|
114
|
+
const actionValue = getSingleLiteralValue(branch.properties.action);
|
|
115
|
+
if (actionValue !== undefined && !actionValues.includes(actionValue)) actionValues.push(actionValue);
|
|
116
|
+
guidance.push(describeRootBranch(index, branch, actionValue));
|
|
117
|
+
} else {
|
|
118
|
+
guidance.push(describeRootBranch(index, branch, undefined));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (actionValues.length > 0) {
|
|
123
|
+
const existingAction = isRecord(baseProperties.action) ? { ...baseProperties.action } : {};
|
|
124
|
+
delete existingAction.const;
|
|
125
|
+
baseProperties.action = { ...existingAction, enum: actionValues };
|
|
126
|
+
}
|
|
127
|
+
result.required = required;
|
|
128
|
+
spillToDescription(result, [
|
|
129
|
+
["rootCombinatorGuidance", guidance],
|
|
130
|
+
...rootCombinators
|
|
131
|
+
.filter(key => key !== flattenedBranches.key)
|
|
132
|
+
.map(key => [key, result[key]] as [string, unknown]),
|
|
133
|
+
]);
|
|
134
|
+
} else {
|
|
135
|
+
spillToDescription(
|
|
136
|
+
result,
|
|
137
|
+
rootCombinators.map(key => [key, result[key]]),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const key of COMBINATOR_KEYS) delete result[key];
|
|
142
|
+
return result;
|
|
143
|
+
}
|