@oh-my-pi/pi-catalog 17.4.0 → 17.4.2

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.
@@ -5,6 +5,17 @@ export interface ModelCacheProviderIdOptions {
5
5
  baseUrl?: string;
6
6
  }
7
7
 
8
+ const CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS: Readonly<Record<string, true>> = {
9
+ "opencode-go": true,
10
+ "opencode-zen": true,
11
+ "github-copilot": true,
12
+ };
13
+
14
+ /** Whether a provider's model-cache namespace requires its resolved credential. */
15
+ export function isCredentialScopedModelCacheProvider(providerId: string): boolean {
16
+ return CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS[providerId] === true;
17
+ }
18
+
8
19
  export function getDefaultModelDiscoveryBaseUrl(providerId: string): string | undefined {
9
20
  switch (providerId) {
10
21
  case "ollama":
@@ -48,15 +59,17 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
48
59
  return "cursor:max-mode-v3";
49
60
  case "litellm": {
50
61
  const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
51
- return `litellm:rich-v5:${Bun.hash(baseUrl).toString(36)}`;
62
+ return `litellm:rich-v6:${Bun.hash(baseUrl).toString(36)}`;
52
63
  }
53
64
  case "opencode-go":
54
65
  case "opencode-zen": {
66
+ // v2: muse-spark-1.2 rows cached before the reasoning/thinking
67
+ // recovery carry `reasoning: false` and must be refetched.
55
68
  const configuredBaseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
56
69
  const trimmedBaseUrl = configuredBaseUrl.endsWith("/") ? configuredBaseUrl.slice(0, -1) : configuredBaseUrl;
57
70
  const discoveryBaseUrl = trimmedBaseUrl.endsWith("/v1") ? trimmedBaseUrl : `${trimmedBaseUrl}/v1`;
58
71
  const scope = `${options.apiKey ?? ""}\u0000${discoveryBaseUrl}`;
59
- return `${providerId}:models-v1:${Bun.hash(scope).toString(36)}`;
72
+ return `${providerId}:models-v2:${Bun.hash(scope).toString(36)}`;
60
73
  }
61
74
  case "github-copilot": {
62
75
  // Copilot model specs bake in the plan-specific endpoint (personal vs
@@ -1,5 +1,6 @@
1
1
  import { fetchAntigravityDiscoveryModels } from "../discovery/antigravity";
2
2
  import { fetchGeminiModels } from "../discovery/gemini";
3
+ import { isGeminiModelId } from "../identity/family";
3
4
  import type { ModelManagerOptions } from "../model-manager";
4
5
  import type { FetchImpl } from "../types";
5
6
  import { GEMINI_CLI_VARIANT_COLLAPSE_TABLE } from "../variant-collapse";
@@ -88,18 +89,19 @@ export function googleGeminiCliModelManagerOptions(
88
89
  fetchDynamicModels: async () => {
89
90
  const models = await fetchAntigravityDiscoveryModels({
90
91
  token,
91
- endpoint,
92
92
  fetcher: toDiscoveryFetch(config?.fetch),
93
93
  collapseTable: GEMINI_CLI_VARIANT_COLLAPSE_TABLE,
94
94
  });
95
95
  if (models === null) {
96
96
  return null;
97
97
  }
98
- return models.map(m => ({
99
- ...m,
100
- provider: "google-gemini-cli" as const,
101
- baseUrl: endpoint,
102
- }));
98
+ return models
99
+ .filter(m => isGeminiModelId(m.id))
100
+ .map(m => ({
101
+ ...m,
102
+ provider: "google-gemini-cli" as const,
103
+ baseUrl: endpoint,
104
+ }));
103
105
  },
104
106
  }
105
107
  : undefined),
@@ -16,6 +16,7 @@ import {
16
16
  isGrokReasoningEffortCapable,
17
17
  isKimiK3ModelId,
18
18
  isKimiModelId,
19
+ isMuseSparkModelId,
19
20
  isQwen38PlusTemplateEffortModelId,
20
21
  isReasoningGlmModelId,
21
22
  } from "../identity/family";
@@ -541,12 +542,15 @@ const OPENAI_NON_RESPONSES_PREFIXES = [
541
542
  "gpt-realtime",
542
543
  ] as const;
543
544
 
544
- function isLikelyOpenAIResponsesModelId(id: string, references: Map<string, ModelSpec<"openai-responses">>): boolean {
545
+ function isLikelyOpenAIResponsesModelId(
546
+ id: string,
547
+ references?: ReadonlyMap<string, ModelSpec<"openai-responses">>,
548
+ ): boolean {
545
549
  const trimmed = id.trim();
546
550
  if (!trimmed) {
547
551
  return false;
548
552
  }
549
- if (references.has(trimmed)) {
553
+ if (references?.has(trimmed)) {
550
554
  return true;
551
555
  }
552
556
  const normalized = trimmed.toLowerCase();
@@ -2621,6 +2625,26 @@ function openCodeModelManagerOptions(
2621
2625
  fallbackApi(defaults.id, base) ??
2622
2626
  defaults.api;
2623
2627
  const baseUrl = openCodeBaseUrlForApi(api, basePath);
2628
+ if (isMuseSparkModelId(defaults.id)) {
2629
+ // Gateway lists these as bare ids with no capability
2630
+ // metadata and no local bundled row, so the generic
2631
+ // defaults would hide the effort dial
2632
+ // (`reasoning: false`). Keep the pinned/fallback route
2633
+ // and restore the documented thinking surface.
2634
+ return {
2635
+ ...(reference ?? defaults),
2636
+ id: defaults.id,
2637
+ name,
2638
+ api,
2639
+ provider: providerId,
2640
+ baseUrl,
2641
+ reasoning: true,
2642
+ input: reference?.input ?? ["text", "image"],
2643
+ thinking: reference?.thinking ?? META_MUSE_SPARK_THINKING,
2644
+ contextWindow: toPositiveNumber(entry.context_length, reference?.contextWindow ?? 1_048_576),
2645
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, reference?.maxTokens ?? 131_072),
2646
+ };
2647
+ }
2624
2648
  if (!reference) {
2625
2649
  return { ...defaults, name, api, baseUrl };
2626
2650
  }
@@ -3296,6 +3320,12 @@ export function vercelAiGatewayModelManagerOptions(
3296
3320
  ): ModelSpec<"anthropic-messages"> => {
3297
3321
  const pricing = entry.pricing as Record<string, unknown> | undefined;
3298
3322
  const tags = Array.isArray(entry.tags) ? (entry.tags as string[]) : [];
3323
+ const reportedMaxTokens = typeof entry.max_tokens === "number" ? entry.max_tokens : defaults.maxTokens;
3324
+ const modelId = typeof entry.id === "string" ? entry.id : defaults.id;
3325
+ const maxTokens =
3326
+ modelId === "meta/muse-spark-1.2-contributor" && typeof reportedMaxTokens === "number"
3327
+ ? Math.min(reportedMaxTokens, 131_072)
3328
+ : reportedMaxTokens;
3299
3329
 
3300
3330
  return {
3301
3331
  ...defaults,
@@ -3310,7 +3340,7 @@ export function vercelAiGatewayModelManagerOptions(
3310
3340
  },
3311
3341
  contextWindow:
3312
3342
  typeof entry.context_window === "number" ? entry.context_window : defaults.contextWindow,
3313
- maxTokens: typeof entry.max_tokens === "number" ? entry.max_tokens : defaults.maxTokens,
3343
+ maxTokens,
3314
3344
  };
3315
3345
  },
3316
3346
  fetch: config?.fetch,
@@ -4629,11 +4659,14 @@ export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
4629
4659
  signal?: AbortSignal;
4630
4660
  timeoutMs?: number;
4631
4661
  referenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;
4662
+ resolveApi?: (entry: Record<string, unknown>, modelId: string) => TApi;
4632
4663
  }
4633
4664
 
4634
4665
  type LiteLLMRichModelEntry = Record<string, unknown>;
4666
+ type LiteLLMApiRoute = "openai" | "other" | "unknown";
4635
4667
  type LiteLLMRichEndpointModel<TApi extends Api> = {
4636
4668
  model: ModelSpec<TApi>;
4669
+ apiRoute: LiteLLMApiRoute;
4637
4670
  supportsVision: unknown;
4638
4671
  supportsReasoning: unknown;
4639
4672
  hasContextWindow: boolean;
@@ -4714,14 +4747,15 @@ function toLiteLLMDisplayName(modelName: string | undefined, referenceName: stri
4714
4747
  return referenceName ? stripLiteLLMResellerUsageSuffix(referenceName) : id;
4715
4748
  }
4716
4749
 
4717
- function mapLiteLLMOpenAICompatibleModel<TApi extends Api>(
4750
+ function mapLiteLLMOpenAICompatibleModel(
4718
4751
  entry: OpenAICompatibleModelRecord,
4719
- defaults: ModelSpec<TApi>,
4720
- reference: ModelSpec<TApi> | undefined,
4721
- ): ModelSpec<TApi> {
4752
+ defaults: ModelSpec<Api>,
4753
+ reference: ModelSpec<Api> | undefined,
4754
+ ): ModelSpec<Api> {
4722
4755
  const model = mapWithBundledReference(entry, defaults, reference);
4723
4756
  return {
4724
4757
  ...model,
4758
+ api: resolveLiteLLMApi(undefined, model.id),
4725
4759
  name: stripLiteLLMResellerUsageSuffix(model.name),
4726
4760
  };
4727
4761
  }
@@ -4808,6 +4842,55 @@ function getSupportedOpenAIParams(entry: LiteLLMRichModelEntry): string[] | unde
4808
4842
  return value.flatMap(item => (typeof item === "string" ? [item] : []));
4809
4843
  }
4810
4844
 
4845
+ function getLiteLLMProviders(entry: LiteLLMRichModelEntry): string[] | undefined {
4846
+ if (!Array.isArray(entry.providers)) {
4847
+ return undefined;
4848
+ }
4849
+ const providers = entry.providers.flatMap(provider => {
4850
+ const normalized = toNonEmptyString(provider)?.toLowerCase();
4851
+ return normalized ? [normalized] : [];
4852
+ });
4853
+ return providers.length > 0 ? providers : undefined;
4854
+ }
4855
+
4856
+ function classifyLiteLLMApiRoute(entry: LiteLLMRichModelEntry | undefined, id: string): LiteLLMApiRoute {
4857
+ if (entry) {
4858
+ const providers = getLiteLLMProviders(entry);
4859
+ if (providers) {
4860
+ return providers.every(provider => provider === "openai") ? "openai" : "other";
4861
+ }
4862
+
4863
+ const params = getLiteLLMParams(entry);
4864
+ const configuredProvider = toNonEmptyString(params?.custom_llm_provider)?.toLowerCase();
4865
+ if (configuredProvider) {
4866
+ return configuredProvider === "openai" ? "openai" : "other";
4867
+ }
4868
+
4869
+ const backendModel = toNonEmptyString(params?.model);
4870
+ const backendSeparator = backendModel?.indexOf("/") ?? -1;
4871
+ if (backendModel && backendSeparator > 0) {
4872
+ return backendModel.slice(0, backendSeparator).toLowerCase() === "openai" ? "openai" : "other";
4873
+ }
4874
+
4875
+ const baseModel = toNonEmptyString(getLiteLLMMetadataValue(entry, "base_model"));
4876
+ const baseSeparator = baseModel?.indexOf("/") ?? -1;
4877
+ if (baseModel && baseSeparator > 0) {
4878
+ return baseModel.slice(0, baseSeparator).toLowerCase() === "openai" ? "openai" : "other";
4879
+ }
4880
+ }
4881
+
4882
+ const modelId = id.toLowerCase().startsWith("openai/") ? id.slice("openai/".length) : id;
4883
+ return isLikelyOpenAIResponsesModelId(modelId) ? "openai" : "unknown";
4884
+ }
4885
+
4886
+ export function resolveLiteLLMApi(
4887
+ entry: Record<string, unknown> | undefined,
4888
+ id: string,
4889
+ fallbackApi: Api = "openai-completions",
4890
+ ): Api {
4891
+ return classifyLiteLLMApiRoute(entry, id) === "openai" ? "openai-responses" : fallbackApi;
4892
+ }
4893
+
4811
4894
  function isLiteLLMUnusableSentinelPlaceholder(entry: LiteLLMRichModelEntry): boolean {
4812
4895
  const modelGroup = toNonEmptyString(entry.model_group);
4813
4896
  const id = toNonEmptyString(entry.id);
@@ -4900,7 +4983,7 @@ function mapLiteLLMRichEntry<TApi extends Api>(
4900
4983
  return {
4901
4984
  id,
4902
4985
  name: toLiteLLMDisplayName(modelName, reference?.name, id),
4903
- api: options.api,
4986
+ api: options.resolveApi?.(entry, id) ?? options.api,
4904
4987
  provider: options.provider,
4905
4988
  baseUrl: runtimeBaseUrl,
4906
4989
  contextWindow,
@@ -4919,6 +5002,34 @@ function mapLiteLLMRichEntry<TApi extends Api>(
4919
5002
  };
4920
5003
  }
4921
5004
 
5005
+ function mergeLiteLLMRichEndpointModels<TApi extends Api>(
5006
+ existing: LiteLLMRichEndpointModel<TApi>,
5007
+ next: LiteLLMRichEndpointModel<TApi>,
5008
+ ): LiteLLMRichEndpointModel<TApi> {
5009
+ const apiRoute =
5010
+ existing.apiRoute === "other" || next.apiRoute === "other"
5011
+ ? "other"
5012
+ : existing.apiRoute === "openai" || next.apiRoute === "openai"
5013
+ ? "openai"
5014
+ : "unknown";
5015
+ const api = next.apiRoute === apiRoute ? next.model.api : existing.model.api;
5016
+ const model: ModelSpec<TApi> = {
5017
+ ...existing.model,
5018
+ api,
5019
+ name: next.model.name === next.model.id ? existing.model.name : next.model.name,
5020
+ contextWindow: next.hasContextWindow ? next.model.contextWindow : existing.model.contextWindow,
5021
+ maxTokens: next.hasMaxTokens ? next.model.maxTokens : existing.model.maxTokens,
5022
+ input: next.supportsVision === true || next.supportsVision === false ? next.model.input : existing.model.input,
5023
+ reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
5024
+ cost: next.hasCost ? next.model.cost : existing.model.cost,
5025
+ compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
5026
+ };
5027
+ if (next.hasToolMetadata) {
5028
+ model.supportsTools = next.model.supportsTools;
5029
+ }
5030
+ return { ...next, apiRoute, model };
5031
+ }
5032
+
4922
5033
  async function fetchLiteLLMRichEndpoint<TApi extends Api>(
4923
5034
  endpoint: string,
4924
5035
  options: FetchLiteLLMRichModelsOptions<TApi>,
@@ -4958,7 +5069,6 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
4958
5069
  return null;
4959
5070
  }
4960
5071
  const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
4961
- let incompleteVisionMetadata = false;
4962
5072
  for (const entry of entries) {
4963
5073
  const model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);
4964
5074
  if (model) {
@@ -4966,11 +5076,9 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
4966
5076
  const supportsReasoning = getLiteLLMMetadataValue(entry, "supports_reasoning");
4967
5077
  const supportsFunctionCalling = getLiteLLMMetadataValue(entry, "supports_function_calling");
4968
5078
  const supportedOpenAIParams = getSupportedOpenAIParams(entry);
4969
- if (supportsVision !== true && supportsVision !== false) {
4970
- incompleteVisionMetadata = true;
4971
- }
4972
- deduped.set(model.id, {
5079
+ const next: LiteLLMRichEndpointModel<TApi> = {
4973
5080
  model,
5081
+ apiRoute: classifyLiteLLMApiRoute(entry, model.id),
4974
5082
  supportsVision,
4975
5083
  supportsReasoning,
4976
5084
  hasContextWindow: toPositiveNumber(getLiteLLMMetadataValue(entry, "max_input_tokens"), null) !== null,
@@ -4981,19 +5089,22 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
4981
5089
  supportedOpenAIParams !== undefined,
4982
5090
  hasSupportedOpenAIParams: supportedOpenAIParams !== undefined,
4983
5091
  hasCost: getLiteLLMCost(entry) !== undefined,
4984
- });
5092
+ };
5093
+ const existing = deduped.get(model.id);
5094
+ deduped.set(model.id, existing ? mergeLiteLLMRichEndpointModels(existing, next) : next);
4985
5095
  }
4986
5096
  }
4987
5097
  if (deduped.size === 0) {
4988
5098
  return null;
4989
5099
  }
5100
+ const models = Array.from(deduped.values()).sort((left, right) => left.model.id.localeCompare(right.model.id));
4990
5101
  return {
4991
- models: Array.from(deduped.values()).sort((left, right) => left.model.id.localeCompare(right.model.id)),
4992
- incompleteVisionMetadata,
5102
+ models,
5103
+ incompleteVisionMetadata: models.some(entry => entry.supportsVision !== true && entry.supportsVision !== false),
4993
5104
  };
4994
5105
  }
4995
5106
 
4996
- export async function fetchLiteLLMRichModels<TApi extends Api>(
5107
+ async function fetchLiteLLMRichModelsInternal<TApi extends Api>(
4997
5108
  options: FetchLiteLLMRichModelsOptions<TApi>,
4998
5109
  ): Promise<ModelSpec<TApi>[] | null> {
4999
5110
  const managementBaseUrl = normalizeLiteLLMManagementBaseUrl(options.baseUrl);
@@ -5033,32 +5144,19 @@ export async function fetchLiteLLMRichModels<TApi extends Api>(
5033
5144
  }
5034
5145
  continue;
5035
5146
  }
5036
- const model: ModelSpec<TApi> = {
5037
- ...existing.model,
5038
- name: next.model.name === next.model.id ? existing.model.name : next.model.name,
5039
- contextWindow: next.hasContextWindow ? next.model.contextWindow : existing.model.contextWindow,
5040
- maxTokens: next.hasMaxTokens ? next.model.maxTokens : existing.model.maxTokens,
5041
- input:
5042
- next.supportsVision === true || next.supportsVision === false
5043
- ? next.model.input
5044
- : existing.model.input,
5045
- reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
5046
- cost: next.hasCost ? next.model.cost : existing.model.cost,
5047
- compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
5048
- };
5049
- if (next.hasToolMetadata) {
5050
- model.supportsTools = next.model.supportsTools;
5051
- }
5052
- deduped.set(next.model.id, { ...next, model });
5147
+ deduped.set(next.model.id, mergeLiteLLMRichEndpointModels(existing, next));
5053
5148
  }
5054
- let hasIncompleteVisionMetadata = false;
5149
+ let needsMoreMetadata = false;
5055
5150
  for (const entry of deduped.values()) {
5056
- if (entry.supportsVision !== true && entry.supportsVision !== false) {
5057
- hasIncompleteVisionMetadata = true;
5151
+ if (
5152
+ (entry.supportsVision !== true && entry.supportsVision !== false) ||
5153
+ (options.resolveApi !== undefined && entry.apiRoute === "unknown")
5154
+ ) {
5155
+ needsMoreMetadata = true;
5058
5156
  break;
5059
5157
  }
5060
5158
  }
5061
- if (!hasIncompleteVisionMetadata) {
5159
+ if (!needsMoreMetadata) {
5062
5160
  break;
5063
5161
  }
5064
5162
  }
@@ -5078,40 +5176,45 @@ export async function fetchLiteLLMRichModels<TApi extends Api>(
5078
5176
  return options.timeoutMs !== undefined ? withCatalogDiscoveryTimeout(options.timeoutMs, fetchModels) : fetchModels();
5079
5177
  }
5080
5178
 
5081
- export function litellmModelManagerOptions(
5082
- config?: LiteLLMModelManagerConfig,
5083
- ): ModelManagerOptions<"openai-completions"> {
5179
+ export async function fetchLiteLLMRichModels<TApi extends Api>(
5180
+ options: FetchLiteLLMRichModelsOptions<TApi>,
5181
+ ): Promise<ModelSpec<TApi>[] | null> {
5182
+ return fetchLiteLLMRichModelsInternal(options);
5183
+ }
5184
+
5185
+ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig): ModelManagerOptions<Api> {
5084
5186
  const apiKey = config?.apiKey;
5085
5187
  const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
5086
5188
  return {
5087
5189
  providerId: "litellm",
5088
- // rich-v5 invalidates rows cached before rich metadata pricing was mapped.
5190
+ // rich-v6 invalidates rows cached before OpenAI models moved to Responses.
5089
5191
  // Earlier versions added bundled reference fallback, continued discovery
5090
5192
  // past incomplete `/model_group/info`, stripped reseller usage suffixes,
5091
- // and filtered placeholder-only `all-team-models` rows. Bump the version
5092
- // whenever the mappers below change, or warm authoritative caches keep
5093
- // serving pre-change rows for the full TTL.
5193
+ // filtered placeholder-only `all-team-models` rows, and mapped rich pricing.
5194
+ // Bump the version whenever the mappers below change, or warm authoritative
5195
+ // caches keep serving pre-change rows for the full TTL.
5094
5196
  cacheProviderId: resolveModelCacheProviderId("litellm", { baseUrl }),
5095
5197
  // litellm is a local-only proxy and is never bundled in models.json (that
5096
5198
  // would leak the machine's localhost catalog). Prefer the proxy's richer
5097
5199
  // management metadata, then enrich ids against models.dev with the bundled
5098
5200
  // catalog as a fallback before using /v1/models.
5099
5201
  fetchDynamicModels: async () => {
5100
- const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
5202
+ const modelsDevReferences = await loadModelsDevReferences<Api>(config?.fetch);
5101
5203
  const resolveReference = createReferenceResolver(modelsDevReferences);
5102
- const richModels = await fetchLiteLLMRichModels({
5204
+ const richModels = await fetchLiteLLMRichModels<Api>({
5103
5205
  api: "openai-completions",
5104
5206
  provider: "litellm",
5105
5207
  baseUrl,
5106
5208
  apiKey,
5107
5209
  fetch: config?.fetch,
5108
5210
  referenceResolver: resolveReference,
5211
+ resolveApi: resolveLiteLLMApi,
5109
5212
  timeoutMs: 10_000,
5110
5213
  });
5111
5214
  if (richModels && richModels.length > 0) {
5112
5215
  return richModels;
5113
5216
  }
5114
- return fetchOpenAICompatibleModels({
5217
+ return fetchOpenAICompatibleModels<Api>({
5115
5218
  api: "openai-completions",
5116
5219
  provider: "litellm",
5117
5220
  baseUrl,
package/src/types.ts CHANGED
@@ -161,7 +161,7 @@ export type OpenAIReasoningDisableMode =
161
161
  | "qwen-enable-thinking-false"
162
162
  | "qwen-template-false";
163
163
 
164
- export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
164
+ export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "qwen" | "thinking";
165
165
 
166
166
  /**
167
167
  * Compatibility settings for openai-completions API.
@@ -965,6 +965,8 @@ export interface Model<TApi extends Api = Api> {
965
965
  preferWebsockets?: boolean;
966
966
  /** Codex Responses Lite transport: send the lite marker and carry instructions/tools as input items (mirrors codex-rs `use_responses_lite`). */
967
967
  useResponsesLite?: boolean;
968
+ /** Codex Code Mode restriction: model expects tools routed through a programmatic exec surface (mirrors codex-rs `tool_mode`). */
969
+ toolMode?: "code_mode_only";
968
970
  /** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
969
971
  contextPromotionTarget?: string;
970
972
  /** Preferred model to use only for compaction (model id or provider/id); the active session model is unchanged. */