@sayknow-cli/ai 0.2.4 → 0.2.5

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/src/models.ts CHANGED
@@ -33,14 +33,21 @@ function getProviderModels(provider: GeneratedProvider): Map<string, Model<Api>>
33
33
  * Mythos rejecting forced tool use) without a full regeneration.
34
34
  */
35
35
  function applyBundledCompatDefaults(model: Model<Api>): Model<Api> {
36
+ let normalized = model;
37
+ if (normalized.id === "minimax-m3" && normalized.name === "MiniMax M3") {
38
+ normalized = { ...normalized, name: "MiniMax-M3" };
39
+ }
36
40
  if (
37
- (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") &&
38
- isClaudeForcedToolChoiceIncapableModelId(model.id) &&
39
- (model.compat as { toolChoiceSupport?: string } | undefined)?.toolChoiceSupport === undefined
41
+ (normalized.api === "anthropic-messages" || normalized.api === "bedrock-converse-stream") &&
42
+ isClaudeForcedToolChoiceIncapableModelId(normalized.id) &&
43
+ (normalized.compat as { toolChoiceSupport?: string } | undefined)?.toolChoiceSupport === undefined
40
44
  ) {
41
- return { ...model, compat: { ...(model.compat ?? {}), toolChoiceSupport: "auto" } as Model<Api>["compat"] };
45
+ return {
46
+ ...normalized,
47
+ compat: { ...(normalized.compat ?? {}), toolChoiceSupport: "auto" } as Model<Api>["compat"],
48
+ };
42
49
  }
43
- return model;
50
+ return normalized;
44
51
  }
45
52
 
46
53
  export type GeneratedProvider = keyof typeof MODELS;
@@ -176,6 +176,57 @@ function mapWithBundledReference<TApi extends Api>(
176
176
  };
177
177
  }
178
178
 
179
+ function getNestedModelValue(entry: OpenAICompatibleModelRecord, path: readonly string[]): unknown {
180
+ let current: unknown = entry;
181
+ for (const segment of path) {
182
+ if (!isRecord(current)) {
183
+ return undefined;
184
+ }
185
+ current = current[segment];
186
+ }
187
+ return current;
188
+ }
189
+
190
+ function firstPositiveModelNumber(fallback: number, ...candidates: readonly unknown[]): number {
191
+ for (const candidate of candidates) {
192
+ const value = toNumber(candidate);
193
+ if (value !== undefined && value > 0) {
194
+ return value;
195
+ }
196
+ }
197
+ return fallback;
198
+ }
199
+
200
+ function mapLmStudioModel(
201
+ entry: OpenAICompatibleModelRecord,
202
+ defaults: Model<"openai-completions">,
203
+ reference: Model<"openai-completions"> | undefined,
204
+ ): Model<"openai-completions"> {
205
+ const model = mapWithBundledReference(entry, defaults, reference);
206
+ return {
207
+ ...model,
208
+ contextWindow: firstPositiveModelNumber(
209
+ model.contextWindow,
210
+ entry.context_length,
211
+ entry.max_context_length,
212
+ getNestedModelValue(entry, ["meta", "n_ctx"]),
213
+ getNestedModelValue(entry, ["details", "context_length"]),
214
+ getNestedModelValue(entry, ["details", "n_ctx"]),
215
+ getNestedModelValue(entry, ["meta", "n_ctx_train"]),
216
+ ),
217
+ maxTokens: firstPositiveModelNumber(
218
+ model.maxTokens,
219
+ entry.max_completion_tokens,
220
+ entry.max_tokens,
221
+ entry.max_output_tokens,
222
+ getNestedModelValue(entry, ["details", "max_completion_tokens"]),
223
+ getNestedModelValue(entry, ["details", "max_tokens"]),
224
+ getNestedModelValue(entry, ["meta", "max_completion_tokens"]),
225
+ getNestedModelValue(entry, ["meta", "max_tokens"]),
226
+ ),
227
+ };
228
+ }
229
+
179
230
  function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
180
231
  const value = baseUrl?.trim();
181
232
  if (!value) {
@@ -761,6 +812,8 @@ function openCodeModelManagerOptions(
761
812
  ): ModelManagerOptions<"openai-completions"> {
762
813
  const apiKey = config?.apiKey;
763
814
  const baseUrl = config?.baseUrl ?? defaultBaseUrl;
815
+ const references =
816
+ providerId === "opencode-go" ? createBundledReferenceMap<"openai-completions">(providerId) : undefined;
764
817
  return {
765
818
  providerId,
766
819
  ...(apiKey && {
@@ -770,6 +823,13 @@ function openCodeModelManagerOptions(
770
823
  provider: providerId,
771
824
  baseUrl,
772
825
  apiKey,
826
+ ...(providerId === "opencode-go" && {
827
+ mapModel: (entry, defaults) => {
828
+ const reference = references?.get(defaults.id);
829
+ const model = mapWithBundledReference(entry, defaults, reference);
830
+ return applyOpenCodeGoOfficialMetadata(model);
831
+ },
832
+ }),
773
833
  }),
774
834
  }),
775
835
  };
@@ -1215,7 +1275,7 @@ export function lmStudioModelManagerOptions(
1215
1275
  apiKey,
1216
1276
  mapModel: (entry, defaults) => {
1217
1277
  const reference = references.get(defaults.id);
1218
- return mapWithBundledReference(entry, defaults, reference);
1278
+ return mapLmStudioModel(entry, defaults, reference);
1219
1279
  },
1220
1280
  }),
1221
1281
  };
@@ -1870,6 +1930,8 @@ export interface ModelsDevProviderDescriptor {
1870
1930
  * Can return null to skip the model, or an array to emit multiple models.
1871
1931
  */
1872
1932
  transformModel?: (model: Model<Api>, modelId: string, raw: ModelsDevModel) => Model<Api> | Model<Api>[] | null;
1933
+ /** Optional static rows appended after mapped models. Used only for official provider catalogs missing from models.dev. */
1934
+ appendModels?: readonly Model<Api>[];
1873
1935
  /**
1874
1936
  * Optional: override the API type per-model.
1875
1937
  * Called with (modelId, raw). Return the API type to use.
@@ -1886,56 +1948,59 @@ export function mapModelsDevToModels(
1886
1948
  const models: Model<Api>[] = [];
1887
1949
  for (const desc of descriptors) {
1888
1950
  const providerData = (data as Record<string, Record<string, unknown>>)[desc.modelsDevKey];
1889
- if (!isRecord(providerData) || !isRecord(providerData.models)) continue;
1890
-
1891
- for (const [modelId, rawModel] of Object.entries(providerData.models)) {
1892
- if (!isRecord(rawModel)) continue;
1893
- const m = rawModel as ModelsDevModel;
1894
-
1895
- // Default filter: tool_call must be true
1896
- if (desc.filterModel) {
1897
- if (!desc.filterModel(modelId, m)) continue;
1898
- } else {
1899
- if (m.tool_call !== true) continue;
1900
- }
1951
+ if (isRecord(providerData) && isRecord(providerData.models)) {
1952
+ for (const [modelId, rawModel] of Object.entries(providerData.models)) {
1953
+ if (!isRecord(rawModel)) continue;
1954
+ const m = rawModel as ModelsDevModel;
1955
+
1956
+ // Default filter: tool_call must be true
1957
+ if (desc.filterModel) {
1958
+ if (!desc.filterModel(modelId, m)) continue;
1959
+ } else {
1960
+ if (m.tool_call !== true) continue;
1961
+ }
1901
1962
 
1902
- // Resolve API and baseUrl (may be per-model for providers like OpenCode)
1903
- const resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };
1904
- if (!resolved) continue;
1905
-
1906
- const mapped: Model<Api> = {
1907
- id: modelId,
1908
- name: toModelName(m.name, modelId),
1909
- api: resolved.api,
1910
- provider: desc.providerId as Model<Api>["provider"],
1911
- baseUrl: resolved.baseUrl,
1912
- reasoning: m.reasoning === true,
1913
- input: toInputCapabilities(m.modalities?.input),
1914
- cost: {
1915
- input: toNumber(m.cost?.input) ?? 0,
1916
- output: toNumber(m.cost?.output) ?? 0,
1917
- cacheRead: toNumber(m.cost?.cache_read) ?? 0,
1918
- cacheWrite: toNumber(m.cost?.cache_write) ?? 0,
1919
- },
1920
- contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? UNK_CONTEXT_WINDOW),
1921
- maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? UNK_MAX_TOKENS),
1922
- ...(desc.compat && { compat: desc.compat }),
1923
- ...(desc.headers && { headers: { ...desc.headers } }),
1924
- };
1963
+ // Resolve API and baseUrl (may be per-model for providers like OpenCode)
1964
+ const resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };
1965
+ if (!resolved) continue;
1966
+
1967
+ const mapped: Model<Api> = {
1968
+ id: modelId,
1969
+ name: toModelName(m.name, modelId),
1970
+ api: resolved.api,
1971
+ provider: desc.providerId as Model<Api>["provider"],
1972
+ baseUrl: resolved.baseUrl,
1973
+ reasoning: m.reasoning === true,
1974
+ input: toInputCapabilities(m.modalities?.input),
1975
+ cost: {
1976
+ input: toNumber(m.cost?.input) ?? 0,
1977
+ output: toNumber(m.cost?.output) ?? 0,
1978
+ cacheRead: toNumber(m.cost?.cache_read) ?? 0,
1979
+ cacheWrite: toNumber(m.cost?.cache_write) ?? 0,
1980
+ },
1981
+ contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? UNK_CONTEXT_WINDOW),
1982
+ maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? UNK_MAX_TOKENS),
1983
+ ...(desc.compat && { compat: desc.compat }),
1984
+ ...(desc.headers && { headers: { ...desc.headers } }),
1985
+ };
1925
1986
 
1926
- // Apply per-model transform
1927
- if (desc.transformModel) {
1928
- const result = desc.transformModel(mapped, modelId, m);
1929
- if (result === null) continue;
1930
- if (Array.isArray(result)) {
1931
- models.push(...result);
1987
+ // Apply per-model transform
1988
+ if (desc.transformModel) {
1989
+ const result = desc.transformModel(mapped, modelId, m);
1990
+ if (result === null) continue;
1991
+ if (Array.isArray(result)) {
1992
+ models.push(...result);
1993
+ } else {
1994
+ models.push(result);
1995
+ }
1932
1996
  } else {
1933
- models.push(result);
1997
+ models.push(mapped);
1934
1998
  }
1935
- } else {
1936
- models.push(mapped);
1937
1999
  }
1938
2000
  }
2001
+ if (desc.appendModels) {
2002
+ models.push(...desc.appendModels);
2003
+ }
1939
2004
  }
1940
2005
  return models;
1941
2006
  }
@@ -2025,22 +2090,35 @@ function createOpenCodeApiResolution(
2025
2090
  };
2026
2091
  }
2027
2092
 
2093
+ const OPENCODE_GO_BASE_PATH = "https://opencode.ai/zen/go";
2028
2094
  const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen");
2029
- // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
2030
- // with `provider.npm = "@ai-sdk/anthropic"`, but the OpenCode Go gateway only
2031
- // serves them at `https://opencode.ai/zen/go/v1/chat/completions` (verified
2032
- // against https://opencode.ai/zen/go/v1/models and the upstream endpoint
2033
- // table at https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the
2034
- // same way and lacks an `npm` field on models.dev so it already falls through
2035
- // to the openai-completions default). Without this override the resolver
2036
- // would POST anthropic-style requests to /v1/messages and the gateway would
2037
- // return its `Page Not Found` HTML (issue #887). Override the resolver so
2038
- // regenerating models.json keeps the correct routing.
2039
- const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", {
2040
- "minimax-m2.7": "openai-completions",
2041
- "qwen3.5-plus": "openai-completions",
2042
- "qwen3.6-plus": "openai-completions",
2043
- });
2095
+ const OPENCODE_GO_CHAT_COMPLETIONS_MODEL_IDS = [
2096
+ "deepseek-v4-flash",
2097
+ "deepseek-v4-pro",
2098
+ "glm-5.1",
2099
+ "glm-5.2",
2100
+ "kimi-k2.6",
2101
+ "kimi-k2.7-code",
2102
+ "mimo-v2.5",
2103
+ "mimo-v2.5-pro",
2104
+ ] as const;
2105
+ const OPENCODE_GO_MESSAGES_MODEL_IDS = [
2106
+ "minimax-m2.5",
2107
+ "minimax-m2.7",
2108
+ "minimax-m3",
2109
+ "qwen3.6-plus",
2110
+ "qwen3.7-max",
2111
+ "qwen3.7-plus",
2112
+ ] as const;
2113
+ const OPENCODE_GO_API_OVERRIDES: Readonly<Record<string, Api>> = {
2114
+ ...Object.fromEntries(OPENCODE_GO_CHAT_COMPLETIONS_MODEL_IDS.map(id => [id, "openai-completions"])),
2115
+ ...Object.fromEntries(OPENCODE_GO_MESSAGES_MODEL_IDS.map(id => [id, "anthropic-messages"])),
2116
+ } as Record<string, Api>;
2117
+ // OpenCode Go has a provider-specific endpoint table at
2118
+ // https://opencode.ai/docs/go/#endpoints. Keep routing aligned with that table:
2119
+ // GLM/Kimi/DeepSeek/MiMo rows use /v1/chat/completions, while MiniMax and
2120
+ // current Qwen Plus/Max rows use /v1/messages via the Anthropic client.
2121
+ const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution(OPENCODE_GO_BASE_PATH, OPENCODE_GO_API_OVERRIDES);
2044
2122
 
2045
2123
  const COPILOT_BASE_URL = "https://api.githubcopilot.com";
2046
2124
 
@@ -2269,6 +2347,215 @@ const filterActiveToolCallModels = (_id: string, m: ModelsDevModel): boolean =>
2269
2347
  return true;
2270
2348
  };
2271
2349
 
2350
+ interface OpenCodeGoOfficialModelMetadata {
2351
+ name: string;
2352
+ contextWindow: number;
2353
+ maxTokens: number;
2354
+ input: ("text" | "image")[];
2355
+ reasoning: boolean;
2356
+ cost: Model["cost"];
2357
+ }
2358
+
2359
+ const OPENCODE_GO_OFFICIAL_MODELS: Readonly<Record<string, OpenCodeGoOfficialModelMetadata>> = {
2360
+ "deepseek-v4-flash": {
2361
+ name: "DeepSeek V4 Flash",
2362
+ contextWindow: 1_000_000,
2363
+ maxTokens: 384_000,
2364
+ input: ["text"],
2365
+ reasoning: true,
2366
+ cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
2367
+ },
2368
+ "deepseek-v4-pro": {
2369
+ name: "DeepSeek V4 Pro",
2370
+ contextWindow: 1_000_000,
2371
+ maxTokens: 384_000,
2372
+ input: ["text"],
2373
+ reasoning: true,
2374
+ cost: { input: 1.74, output: 3.48, cacheRead: 0.0145, cacheWrite: 0 },
2375
+ },
2376
+ "glm-5": {
2377
+ name: "GLM-5",
2378
+ contextWindow: 204_800,
2379
+ maxTokens: 131_072,
2380
+ input: ["text"],
2381
+ reasoning: true,
2382
+ cost: { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 },
2383
+ },
2384
+ "glm-5.1": {
2385
+ name: "GLM-5.1",
2386
+ contextWindow: 200_000,
2387
+ maxTokens: 131_072,
2388
+ input: ["text"],
2389
+ reasoning: true,
2390
+ cost: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
2391
+ },
2392
+ "glm-5.2": {
2393
+ name: "GLM-5.2",
2394
+ contextWindow: 1_000_000,
2395
+ maxTokens: 131_072,
2396
+ input: ["text"],
2397
+ reasoning: true,
2398
+ cost: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
2399
+ },
2400
+ "kimi-k2.5": {
2401
+ name: "Kimi K2.5",
2402
+ contextWindow: 262_144,
2403
+ maxTokens: 262_144,
2404
+ input: ["text", "image"],
2405
+ reasoning: true,
2406
+ cost: { input: 0.3, output: 1.9, cacheRead: 0, cacheWrite: 0 },
2407
+ },
2408
+ "kimi-k2.6": {
2409
+ name: "Kimi K2.6",
2410
+ contextWindow: 262_144,
2411
+ maxTokens: 262_144,
2412
+ input: ["text", "image"],
2413
+ reasoning: true,
2414
+ cost: { input: 0.95, output: 4, cacheRead: 0.2, cacheWrite: 0 },
2415
+ },
2416
+ "kimi-k2.7-code": {
2417
+ name: "Kimi K2.7 Code",
2418
+ contextWindow: 262_144,
2419
+ maxTokens: 262_144,
2420
+ input: ["text", "image"],
2421
+ reasoning: true,
2422
+ cost: { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
2423
+ },
2424
+ "minimax-m2.5": {
2425
+ name: "MiniMax M2.5",
2426
+ contextWindow: 204_800,
2427
+ maxTokens: 131_072,
2428
+ input: ["text"],
2429
+ reasoning: true,
2430
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
2431
+ },
2432
+ "minimax-m2.7": {
2433
+ name: "MiniMax M2.7",
2434
+ contextWindow: 204_800,
2435
+ maxTokens: 131_072,
2436
+ input: ["text"],
2437
+ reasoning: true,
2438
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
2439
+ },
2440
+ "minimax-m3": {
2441
+ name: "MiniMax M3",
2442
+ contextWindow: 512_000,
2443
+ maxTokens: 128_000,
2444
+ input: ["text", "image"],
2445
+ reasoning: true,
2446
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
2447
+ },
2448
+ "qwen3.5-plus": {
2449
+ name: "Qwen3.5 Plus",
2450
+ contextWindow: 1_000_000,
2451
+ maxTokens: 65_536,
2452
+ input: ["text", "image"],
2453
+ reasoning: true,
2454
+ cost: { input: 0.4, output: 2.4, cacheRead: 0, cacheWrite: 0 },
2455
+ },
2456
+ "qwen3.6-plus": {
2457
+ name: "Qwen3.6 Plus",
2458
+ contextWindow: 1_000_000,
2459
+ maxTokens: 65_536,
2460
+ input: ["text", "image"],
2461
+ reasoning: true,
2462
+ cost: { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 2.5 },
2463
+ },
2464
+ "qwen3.7-max": {
2465
+ name: "Qwen3.7 Max",
2466
+ contextWindow: 1_000_000,
2467
+ maxTokens: 65_536,
2468
+ input: ["text"],
2469
+ reasoning: true,
2470
+ cost: { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.125 },
2471
+ },
2472
+ "qwen3.7-plus": {
2473
+ name: "Qwen3.7 Plus",
2474
+ contextWindow: 1_000_000,
2475
+ maxTokens: 64_000,
2476
+ input: ["text", "image"],
2477
+ reasoning: true,
2478
+ cost: { input: 1.2, output: 4.8, cacheRead: 0.12, cacheWrite: 1.5 },
2479
+ },
2480
+ "mimo-v2-omni": {
2481
+ name: "MiMo-V2-Omni",
2482
+ contextWindow: 262_144,
2483
+ maxTokens: 131_072,
2484
+ input: ["text", "image"],
2485
+ reasoning: true,
2486
+ cost: { input: 0.4, output: 2, cacheRead: 0.08, cacheWrite: 0 },
2487
+ },
2488
+ "mimo-v2-pro": {
2489
+ name: "MiMo-V2-Pro",
2490
+ contextWindow: 1_048_576,
2491
+ maxTokens: 131_072,
2492
+ input: ["text"],
2493
+ reasoning: true,
2494
+ cost: { input: 1, output: 3, cacheRead: 0.2, cacheWrite: 0 },
2495
+ },
2496
+ "mimo-v2.5": {
2497
+ name: "MiMo-V2.5",
2498
+ contextWindow: 1_048_576,
2499
+ maxTokens: 131_072,
2500
+ input: ["text", "image"],
2501
+ reasoning: true,
2502
+ cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
2503
+ },
2504
+ "mimo-v2.5-pro": {
2505
+ name: "MiMo-V2.5-Pro",
2506
+ contextWindow: 1_048_576,
2507
+ maxTokens: 131_072,
2508
+ input: ["text"],
2509
+ reasoning: true,
2510
+ cost: { input: 1.74, output: 3.48, cacheRead: 0.0145, cacheWrite: 0 },
2511
+ },
2512
+ "hy3-preview": {
2513
+ name: "Hy3 preview",
2514
+ contextWindow: 256_000,
2515
+ maxTokens: 64_000,
2516
+ input: ["text"],
2517
+ reasoning: true,
2518
+ cost: { input: 0.066, output: 0.26, cacheRead: 0.029, cacheWrite: 0 },
2519
+ },
2520
+ };
2521
+
2522
+ function applyOpenCodeGoOfficialMetadata<TApi extends Api>(model: Model<TApi>): Model<TApi> {
2523
+ const metadata = OPENCODE_GO_OFFICIAL_MODELS[model.id];
2524
+ if (!metadata) return model;
2525
+ return {
2526
+ ...model,
2527
+ name: metadata.name,
2528
+ reasoning: metadata.reasoning,
2529
+ input: [...metadata.input],
2530
+ cost: { ...metadata.cost },
2531
+ contextWindow: metadata.contextWindow,
2532
+ maxTokens: metadata.maxTokens,
2533
+ };
2534
+ }
2535
+
2536
+ function createOpenCodeGoOfficialModels(): Model<Api>[] {
2537
+ return Object.entries(OPENCODE_GO_OFFICIAL_MODELS).map(([id, metadata]) => {
2538
+ const resolved = resolveApiByRules(
2539
+ id,
2540
+ {},
2541
+ OPENCODE_GO_API_RESOLUTION.rules,
2542
+ OPENCODE_GO_API_RESOLUTION.defaultResolution,
2543
+ );
2544
+ return {
2545
+ id,
2546
+ name: metadata.name,
2547
+ api: resolved.api,
2548
+ provider: "opencode-go",
2549
+ baseUrl: resolved.baseUrl,
2550
+ reasoning: metadata.reasoning,
2551
+ input: [...metadata.input],
2552
+ cost: { ...metadata.cost },
2553
+ contextWindow: metadata.contextWindow,
2554
+ maxTokens: metadata.maxTokens,
2555
+ };
2556
+ });
2557
+ }
2558
+
2272
2559
  const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDescriptor[] = [
2273
2560
  // --- Cloudflare AI Gateway ---
2274
2561
  anthropicMessagesDescriptor(
@@ -2299,6 +2586,8 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
2299
2586
  OPENCODE_GO_API_RESOLUTION.rules,
2300
2587
  OPENCODE_GO_API_RESOLUTION.defaultResolution,
2301
2588
  ),
2589
+ transformModel: model => applyOpenCodeGoOfficialMetadata(model),
2590
+ appendModels: createOpenCodeGoOfficialModels(),
2302
2591
  }),
2303
2592
  // --- GitHub Copilot ---
2304
2593
  openAiCompletionsDescriptor("github-copilot", "github-copilot", COPILOT_BASE_URL, {
@@ -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/wire";
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 { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
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
@@ -2573,111 +2563,6 @@ export function normalizeAnthropicToolSchema(schema: unknown): unknown {
2573
2563
  return result;
2574
2564
  }
2575
2565
 
2576
- function getRequiredNames(schema: Record<string, unknown>): Set<string> {
2577
- return new Set(
2578
- Array.isArray(schema.required)
2579
- ? schema.required.filter((entry): entry is string => typeof entry === "string")
2580
- : [],
2581
- );
2582
- }
2583
-
2584
- function getSingleLiteralValue(schema: unknown): unknown | undefined {
2585
- if (!isRecord(schema)) return undefined;
2586
- if (Object.hasOwn(schema, "const")) return schema.const;
2587
- if (Array.isArray(schema.enum) && schema.enum.length === 1) return schema.enum[0];
2588
- return undefined;
2589
- }
2590
-
2591
- function describeAnthropicRootBranch(index: number, branch: Record<string, unknown>, action: unknown): string {
2592
- const required = [...getRequiredNames(branch)];
2593
- const parts = [`Branch ${index + 1}`];
2594
- if (typeof action === "string" || typeof action === "number" || typeof action === "boolean") {
2595
- parts.push(`action ${JSON.stringify(action)}`);
2596
- }
2597
- if (required.length > 0) parts.push(`branch-required fields: ${required.join(", ")}`);
2598
- if (typeof branch.description === "string" && branch.description.length > 0) parts.push(branch.description);
2599
- return parts.join("; ");
2600
- }
2601
- function collectAnthropicRootObjectBranches(schema: unknown): Record<string, unknown>[] | undefined {
2602
- if (!isRecord(schema)) return undefined;
2603
- if (isJsonSchemaObjectNode(schema)) return [schema];
2604
-
2605
- const combinatorKeys = COMBINATOR_KEYS.filter(key => Array.isArray(schema[key]));
2606
- if (combinatorKeys.length === 0) return undefined;
2607
-
2608
- const branches: Record<string, unknown>[] = [];
2609
- for (const key of combinatorKeys) {
2610
- const variants = schema[key];
2611
- if (!Array.isArray(variants) || variants.length === 0) return undefined;
2612
- for (const variant of variants) {
2613
- const nestedBranches = collectAnthropicRootObjectBranches(variant);
2614
- if (nestedBranches === undefined) return undefined;
2615
- branches.push(...nestedBranches);
2616
- }
2617
- }
2618
- return branches;
2619
- }
2620
-
2621
- /**
2622
- * Anthropic rejects tool `input_schema` roots containing top-level oneOf/anyOf/allOf.
2623
- * Keep the generic normalizer schema-preserving, then flatten only provider-emitted
2624
- * tool roots into one object while leaving nested combinators untouched.
2625
- */
2626
- export function normalizeAnthropicToolRootInputSchema(schema: Record<string, unknown>): Record<string, unknown> {
2627
- const result: Record<string, unknown> = { ...schema };
2628
- const rootCombinators = COMBINATOR_KEYS.filter(key => Array.isArray(result[key]));
2629
- if (rootCombinators.length === 0) return result;
2630
-
2631
- const baseProperties = isRecord(result.properties) ? { ...result.properties } : {};
2632
- const flattenedBranches = rootCombinators
2633
- .map(key => ({ key, branches: collectAnthropicRootObjectBranches({ [key]: result[key] }) }))
2634
- .find(entry => entry.branches !== undefined && entry.branches.length > 0);
2635
-
2636
- result.type = "object";
2637
- result.properties = baseProperties;
2638
- result.additionalProperties = result.additionalProperties === undefined ? false : result.additionalProperties;
2639
-
2640
- if (flattenedBranches?.branches !== undefined) {
2641
- const variants = flattenedBranches.branches;
2642
- const commonRequired = variants.map(variant => getRequiredNames(variant));
2643
- const required = [...commonRequired[0]].filter(name => commonRequired.every(set => set.has(name)));
2644
- const actionValues: unknown[] = [];
2645
- const guidance: string[] = [];
2646
-
2647
- for (const [index, branch] of variants.entries()) {
2648
- if (isRecord(branch.properties)) {
2649
- Object.assign(baseProperties, branch.properties);
2650
- const actionValue = getSingleLiteralValue(branch.properties.action);
2651
- if (actionValue !== undefined && !actionValues.includes(actionValue)) actionValues.push(actionValue);
2652
- guidance.push(describeAnthropicRootBranch(index, branch, actionValue));
2653
- } else {
2654
- guidance.push(describeAnthropicRootBranch(index, branch, undefined));
2655
- }
2656
- }
2657
-
2658
- if (actionValues.length > 0) {
2659
- const existingAction = isRecord(baseProperties.action) ? { ...baseProperties.action } : {};
2660
- delete existingAction.const;
2661
- baseProperties.action = { ...existingAction, enum: actionValues };
2662
- }
2663
- result.required = required;
2664
- spillToDescription(result, [
2665
- ["rootCombinatorGuidance", guidance],
2666
- ...rootCombinators
2667
- .filter(key => key !== flattenedBranches.key)
2668
- .map(key => [key, result[key]] as [string, unknown]),
2669
- ]);
2670
- } else {
2671
- spillToDescription(
2672
- result,
2673
- rootCombinators.map(key => [key, result[key]]),
2674
- );
2675
- }
2676
-
2677
- for (const key of COMBINATOR_KEYS) delete result[key];
2678
- return result;
2679
- }
2680
-
2681
2566
  type AnthropicToolInputSchema = Anthropic.Messages.Tool["input_schema"];
2682
2567
 
2683
2568
  type AnthropicToolSchemaPlan = {
@@ -2845,7 +2730,7 @@ function normalizeAnthropicStrictSchema(
2845
2730
 
2846
2731
  function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
2847
2732
  const jsonSchema = toolWireSchema(tool);
2848
- return normalizeAnthropicToolRootInputSchema(
2733
+ return flattenToolRootCombinators(
2849
2734
  normalizeAnthropicToolSchema({
2850
2735
  ...jsonSchema,
2851
2736
  type: "object",