@f5-sales-demo/pi-ai 19.51.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.
- package/CHANGELOG.md +1997 -0
- package/README.md +1160 -0
- package/package.json +135 -0
- package/src/api-registry.ts +95 -0
- package/src/auth-storage.ts +2694 -0
- package/src/cli.ts +493 -0
- package/src/index.ts +42 -0
- package/src/model-cache.ts +97 -0
- package/src/model-manager.ts +349 -0
- package/src/model-thinking.ts +561 -0
- package/src/models.json +49439 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +81 -0
- package/src/provider-models/descriptors.ts +285 -0
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +4 -0
- package/src/provider-models/openai-compat.ts +2074 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/amazon-bedrock.ts +706 -0
- package/src/providers/anthropic.ts +1682 -0
- package/src/providers/azure-openai-responses.ts +391 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2218 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +381 -0
- package/src/providers/google-gemini-cli.ts +1133 -0
- package/src/providers/google-shared.ts +354 -0
- package/src/providers/google-vertex.ts +436 -0
- package/src/providers/google.ts +381 -0
- package/src/providers/kimi.ts +151 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +158 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2345 -0
- package/src/providers/openai-completions-compat.ts +159 -0
- package/src/providers/openai-completions.ts +1290 -0
- package/src/providers/openai-responses-shared.ts +452 -0
- package/src/providers/openai-responses.ts +519 -0
- package/src/providers/register-builtins.ts +329 -0
- package/src/providers/synthetic.ts +154 -0
- package/src/providers/transform-messages.ts +234 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +728 -0
- package/src/types.ts +546 -0
- package/src/usage/claude.ts +337 -0
- package/src/usage/gemini.ts +248 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +200 -0
- package/src/usage/kimi.ts +286 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +387 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +130 -0
- package/src/utils/abort.ts +36 -0
- package/src/utils/anthropic-auth.ts +293 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +209 -0
- package/src/utils/http-inspector.ts +165 -0
- package/src/utils/idle-iterator.ts +176 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +134 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +59 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/github-copilot.ts +358 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +275 -0
- package/src/utils/oauth/google-gemini-cli.ts +334 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +512 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +251 -0
- package/src/utils/oauth/litellm.ts +81 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +59 -0
- package/src/utils/oauth/nanogpt.ts +51 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +190 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +200 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +60 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +59 -0
- package/src/utils/oauth/types.ts +89 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +88 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +51 -0
- package/src/utils/overflow.ts +134 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +93 -0
- package/src/utils/schema/CONSTRAINTS.md +160 -0
- package/src/utils/schema/adapt.ts +20 -0
- package/src/utils/schema/compatibility.ts +397 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/equality.ts +93 -0
- package/src/utils/schema/fields.ts +147 -0
- package/src/utils/schema/index.ts +9 -0
- package/src/utils/schema/normalize-cca.ts +479 -0
- package/src/utils/schema/sanitize-google.ts +212 -0
- package/src/utils/schema/strict-mode.ts +385 -0
- package/src/utils/schema/types.ts +5 -0
- package/src/utils/tool-choice.ts +81 -0
- package/src/utils/validation.ts +664 -0
- package/src/utils.ts +147 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { resolveOpenAICompat } from "./providers/openai-completions-compat";
|
|
2
|
+
import type { Api, Model as ApiModel, ThinkingConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
/** User-facing thinking levels, ordered least to most intensive. */
|
|
5
|
+
export const enum Effort {
|
|
6
|
+
Minimal = "minimal",
|
|
7
|
+
Low = "low",
|
|
8
|
+
Medium = "medium",
|
|
9
|
+
High = "high",
|
|
10
|
+
XHigh = "xhigh",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const THINKING_EFFORTS: readonly Effort[] = [
|
|
14
|
+
Effort.Minimal,
|
|
15
|
+
Effort.Low,
|
|
16
|
+
Effort.Medium,
|
|
17
|
+
Effort.High,
|
|
18
|
+
Effort.XHigh,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
22
|
+
const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [
|
|
23
|
+
Effort.Minimal,
|
|
24
|
+
Effort.Low,
|
|
25
|
+
Effort.Medium,
|
|
26
|
+
Effort.High,
|
|
27
|
+
Effort.XHigh,
|
|
28
|
+
];
|
|
29
|
+
const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
|
|
30
|
+
const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
31
|
+
const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
32
|
+
const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High];
|
|
33
|
+
const CLOUDFLARE_AI_GATEWAY_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic";
|
|
34
|
+
|
|
35
|
+
type SemVer = {
|
|
36
|
+
major: number;
|
|
37
|
+
minor: number;
|
|
38
|
+
patch: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type GeminiKind = "pro" | "flash";
|
|
42
|
+
type AnthropicKind = "opus" | "sonnet";
|
|
43
|
+
type OpenAIVariant = "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "mini" | "max" | "nano";
|
|
44
|
+
|
|
45
|
+
const CODEX_GPT_5_4_PRIORITY_BY_VARIANT: Partial<Record<OpenAIVariant, number>> = {
|
|
46
|
+
base: 0,
|
|
47
|
+
mini: 1,
|
|
48
|
+
nano: 2,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
interface GeminiModel {
|
|
52
|
+
family: "gemini";
|
|
53
|
+
kind: GeminiKind;
|
|
54
|
+
version: SemVer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface AnthropicModel {
|
|
58
|
+
family: "anthropic";
|
|
59
|
+
kind: AnthropicKind;
|
|
60
|
+
version: SemVer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface OpenAIModel {
|
|
64
|
+
family: "openai";
|
|
65
|
+
variant: OpenAIVariant;
|
|
66
|
+
version: SemVer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface UnknownModel {
|
|
70
|
+
family: "unknown";
|
|
71
|
+
id: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Static fallback model injected when Cloudflare AI Gateway discovery
|
|
78
|
+
* returns no results. Ensures the provider always has at least one usable
|
|
79
|
+
* model entry in the catalog.
|
|
80
|
+
*/
|
|
81
|
+
export const CLOUDFLARE_FALLBACK_MODEL: ApiModel<"anthropic-messages"> = {
|
|
82
|
+
id: "claude-sonnet-4-5",
|
|
83
|
+
name: "Claude Sonnet 4.5",
|
|
84
|
+
api: "anthropic-messages",
|
|
85
|
+
provider: "cloudflare-ai-gateway",
|
|
86
|
+
baseUrl: CLOUDFLARE_AI_GATEWAY_BASE_URL,
|
|
87
|
+
reasoning: true,
|
|
88
|
+
input: ["text", "image"],
|
|
89
|
+
cost: {
|
|
90
|
+
input: 3,
|
|
91
|
+
output: 15,
|
|
92
|
+
cacheRead: 0.3,
|
|
93
|
+
cacheWrite: 3.75,
|
|
94
|
+
},
|
|
95
|
+
contextWindow: 200000,
|
|
96
|
+
maxTokens: 64000,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns a copy of the model with canonical thinking metadata attached.
|
|
101
|
+
*
|
|
102
|
+
* This helper belongs to catalog enrichment only. Runtime consumers should
|
|
103
|
+
* trust `model.thinking` and avoid inferring capabilities on demand.
|
|
104
|
+
*/
|
|
105
|
+
export function enrichModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi> {
|
|
106
|
+
const normalizedThinking = normalizeThinkingConfig(model.thinking);
|
|
107
|
+
if (!model.reasoning) {
|
|
108
|
+
return normalizedThinking === undefined && model.thinking === undefined
|
|
109
|
+
? model
|
|
110
|
+
: { ...model, thinking: undefined };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const thinking = normalizedThinking ?? inferModelThinking(model);
|
|
114
|
+
if (thinkingsEqual(normalizedThinking, thinking)) {
|
|
115
|
+
return model;
|
|
116
|
+
}
|
|
117
|
+
return { ...model, thinking };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns a copy of the model with thinking metadata recomputed from the
|
|
122
|
+
* canonical rules, replacing any existing `thinking`.
|
|
123
|
+
*/
|
|
124
|
+
export function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi> {
|
|
125
|
+
if (!model.reasoning) {
|
|
126
|
+
const normalizedThinking = normalizeThinkingConfig(model.thinking);
|
|
127
|
+
return normalizedThinking === undefined && model.thinking === undefined
|
|
128
|
+
? model
|
|
129
|
+
: { ...model, thinking: undefined };
|
|
130
|
+
}
|
|
131
|
+
return { ...model, thinking: inferModelThinking(model) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply upstream metadata corrections to a mutable array of models.
|
|
136
|
+
*
|
|
137
|
+
* Each model is first normalized through `refreshModelThinking()` so generated
|
|
138
|
+
* catalogs keep canonical thinking metadata and policy fixes in one pass.
|
|
139
|
+
*/
|
|
140
|
+
export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
|
|
141
|
+
for (let index = 0; index < models.length; index++) {
|
|
142
|
+
const model = refreshModelThinking(models[index]!);
|
|
143
|
+
applyGeneratedModelPolicy(model);
|
|
144
|
+
models[index] = model;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Link `-spark` model variants to their base models for context promotion.
|
|
150
|
+
*
|
|
151
|
+
* When a spark model's context is exhausted, the agent can promote to the
|
|
152
|
+
* corresponding full model. This sets `contextPromotionTarget` on each
|
|
153
|
+
* spark variant that has a matching base model.
|
|
154
|
+
*/
|
|
155
|
+
export function linkSparkPromotionTargets(models: ApiModel<Api>[]): void {
|
|
156
|
+
for (const candidate of models) {
|
|
157
|
+
const parsedCandidate = parseKnownModel(candidate.id);
|
|
158
|
+
if (parsedCandidate.family !== "openai" || parsedCandidate.variant !== "codex-spark") continue;
|
|
159
|
+
const baseId = candidate.id.slice(0, -"-spark".length);
|
|
160
|
+
const fallback = models.find(
|
|
161
|
+
model => model.provider === candidate.provider && model.api === candidate.api && model.id === baseId,
|
|
162
|
+
);
|
|
163
|
+
if (!fallback) continue;
|
|
164
|
+
candidate.contextPromotionTarget = `${fallback.provider}/${fallback.id}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns supported thinking efforts from canonical model rules constrained by
|
|
170
|
+
* explicit model metadata.
|
|
171
|
+
*
|
|
172
|
+
* @throws Error when a reasoning-capable model is missing thinking metadata
|
|
173
|
+
*/
|
|
174
|
+
export function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[] {
|
|
175
|
+
if (!model.reasoning) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
if (!model.thinking) {
|
|
179
|
+
throw new Error(`Model ${model.provider}/${model.id} is missing thinking metadata`);
|
|
180
|
+
}
|
|
181
|
+
const configuredEfforts = expandEffortRange(model.thinking);
|
|
182
|
+
const parsedModel = parseKnownModel(model.id);
|
|
183
|
+
if (parsedModel.family === "unknown") {
|
|
184
|
+
return configuredEfforts;
|
|
185
|
+
}
|
|
186
|
+
return intersectEfforts(configuredEfforts, inferSupportedEfforts(parsedModel, model));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clamps a requested thinking level against explicit model metadata.
|
|
191
|
+
*
|
|
192
|
+
* Non-reasoning models always resolve to `undefined`.
|
|
193
|
+
*/
|
|
194
|
+
export function clampThinkingLevelForModel<TApi extends Api>(
|
|
195
|
+
model: ApiModel<TApi> | undefined,
|
|
196
|
+
requested: Effort | undefined,
|
|
197
|
+
): Effort | undefined {
|
|
198
|
+
if (!model) {
|
|
199
|
+
return requested;
|
|
200
|
+
}
|
|
201
|
+
if (!model.reasoning || requested === undefined) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const levels = getSupportedEfforts(model);
|
|
206
|
+
if (levels.includes(requested)) {
|
|
207
|
+
return requested;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const requestedIndex = THINKING_EFFORTS.indexOf(requested);
|
|
211
|
+
if (requestedIndex === -1) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let clamped: Effort | undefined;
|
|
216
|
+
for (const effort of levels) {
|
|
217
|
+
if (THINKING_EFFORTS.indexOf(effort) > requestedIndex) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
clamped = effort;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return clamped ?? levels[0];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): Effort {
|
|
227
|
+
if (!model.reasoning) {
|
|
228
|
+
throw new Error(`Model ${model.provider}/${model.id} does not support thinking`);
|
|
229
|
+
}
|
|
230
|
+
const levels = getSupportedEfforts(model);
|
|
231
|
+
if (!levels.includes(effort)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Thinking effort ${effort} is not supported by ${model.provider}/${model.id}. Supported efforts: ${levels.join(", ")}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return effort;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Maps a normalized thinking effort to Google's `thinkingLevel` enum values. */
|
|
240
|
+
export function mapEffortToGoogleThinkingLevel<TApi extends Api>(
|
|
241
|
+
model: ApiModel<TApi>,
|
|
242
|
+
effort: Effort,
|
|
243
|
+
): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" {
|
|
244
|
+
switch (requireSupportedEffort(model, effort)) {
|
|
245
|
+
case Effort.Minimal:
|
|
246
|
+
return "MINIMAL";
|
|
247
|
+
case Effort.Low:
|
|
248
|
+
return "LOW";
|
|
249
|
+
case Effort.Medium:
|
|
250
|
+
return "MEDIUM";
|
|
251
|
+
case Effort.High:
|
|
252
|
+
case Effort.XHigh:
|
|
253
|
+
return "HIGH";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Maps a normalized thinking effort to Anthropic adaptive effort values. */
|
|
258
|
+
export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
|
|
259
|
+
model: ApiModel<TApi>,
|
|
260
|
+
effort: Effort,
|
|
261
|
+
): "low" | "medium" | "high" | "max" {
|
|
262
|
+
switch (requireSupportedEffort(model, effort)) {
|
|
263
|
+
case Effort.Minimal:
|
|
264
|
+
case Effort.Low:
|
|
265
|
+
return "low";
|
|
266
|
+
case Effort.Medium:
|
|
267
|
+
return "medium";
|
|
268
|
+
case Effort.High:
|
|
269
|
+
return "high";
|
|
270
|
+
case Effort.XHigh:
|
|
271
|
+
return "max";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
|
|
276
|
+
const parsedModel = parseKnownModel(model.id);
|
|
277
|
+
if (parsedModel.family === "anthropic") {
|
|
278
|
+
applyAnthropicCatalogPolicy(model, parsedModel);
|
|
279
|
+
}
|
|
280
|
+
if (parsedModel.family === "openai") {
|
|
281
|
+
applyOpenAICatalogPolicy(model, parsedModel);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function applyAnthropicCatalogPolicy(model: ApiModel<Api>, parsedModel: AnthropicModel): void {
|
|
286
|
+
// Claude Opus 4.5: models.dev reports 3x the correct cache pricing.
|
|
287
|
+
if (model.provider === "anthropic" && parsedModel.kind === "opus" && semverEqual(parsedModel.version, "4.5")) {
|
|
288
|
+
model.cost.cacheRead = 0.5;
|
|
289
|
+
model.cost.cacheWrite = 6.25;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Bedrock Opus 4.6: upstream metadata is stale for cache pricing and context.
|
|
293
|
+
if (model.provider === "amazon-bedrock" && parsedModel.kind === "opus" && semverEqual(parsedModel.version, "4.6")) {
|
|
294
|
+
model.cost.cacheRead = 0.5;
|
|
295
|
+
model.cost.cacheWrite = 6.25;
|
|
296
|
+
model.contextWindow = 1000000;
|
|
297
|
+
model.maxTokens = 128000;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function applyOpenAICatalogPolicy(model: ApiModel<Api>, parsedModel: OpenAIModel): void {
|
|
302
|
+
// Codex models: 400K figure includes output budget; input window is 272K.
|
|
303
|
+
if (parsedModel.variant.startsWith("codex") && parsedModel.variant !== "codex-spark") {
|
|
304
|
+
model.contextWindow = 272000;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// GPT-5.4 mini/nano use plain OpenAI IDs on the Codex transport, but Codex still
|
|
308
|
+
// enforces the lower prompt budget for these variants. Codex discovery can also
|
|
309
|
+
// report inconsistent priorities for the GPT-5.4 family, so normalize by parsed
|
|
310
|
+
// variant instead of special-casing raw model ids.
|
|
311
|
+
if (model.api === "openai-codex-responses" && semverEqual(parsedModel.version, "5.4")) {
|
|
312
|
+
const normalizedPriority = CODEX_GPT_5_4_PRIORITY_BY_VARIANT[parsedModel.variant];
|
|
313
|
+
if (normalizedPriority !== undefined) {
|
|
314
|
+
model.priority = normalizedPriority;
|
|
315
|
+
}
|
|
316
|
+
if (parsedModel.variant === "mini" || parsedModel.variant === "nano") {
|
|
317
|
+
model.contextWindow = 272000;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingConfig {
|
|
323
|
+
const parsedModel = parseKnownModel(model.id);
|
|
324
|
+
const efforts = inferSupportedEfforts(parsedModel, model);
|
|
325
|
+
const minLevel = efforts[0];
|
|
326
|
+
const maxLevel = efforts.at(-1);
|
|
327
|
+
if (!minLevel || !maxLevel) {
|
|
328
|
+
throw new Error(`Model ${model.provider}/${model.id} resolved to an empty thinking range`);
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
mode: inferThinkingControlMode(model, parsedModel),
|
|
332
|
+
minLevel,
|
|
333
|
+
maxLevel,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function normalizeThinkingConfig(thinking: ThinkingConfig | undefined): ThinkingConfig | undefined {
|
|
338
|
+
if (!thinking || expandEffortRange(thinking).length === 0) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return thinking;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function thinkingsEqual(left: ThinkingConfig | undefined, right: ThinkingConfig | undefined): boolean {
|
|
345
|
+
if (left === right) return true;
|
|
346
|
+
if (!left || !right) return false;
|
|
347
|
+
return left.mode === right.mode && left.minLevel === right.minLevel && left.maxLevel === right.maxLevel;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function expandEffortRange(thinking: ThinkingConfig): readonly Effort[] {
|
|
351
|
+
const minIndex = THINKING_EFFORTS.indexOf(thinking.minLevel);
|
|
352
|
+
const maxIndex = THINKING_EFFORTS.indexOf(thinking.maxLevel);
|
|
353
|
+
if (minIndex === -1 || maxIndex === -1 || minIndex > maxIndex) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
return THINKING_EFFORTS.slice(minIndex, maxIndex + 1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function intersectEfforts(left: readonly Effort[], right: readonly Effort[]): readonly Effort[] {
|
|
360
|
+
return left.filter(effort => right.includes(effort));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model: ApiModel<TApi>): readonly Effort[] {
|
|
364
|
+
switch (parsedModel.family) {
|
|
365
|
+
case "openai":
|
|
366
|
+
return inferOpenAISupportedEfforts(parsedModel);
|
|
367
|
+
case "gemini":
|
|
368
|
+
return inferGeminiSupportedEfforts(parsedModel);
|
|
369
|
+
case "anthropic":
|
|
370
|
+
return inferAnthropicSupportedEfforts(parsedModel, model);
|
|
371
|
+
case "unknown":
|
|
372
|
+
return inferFallbackEfforts(model);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function inferOpenAISupportedEfforts(model: OpenAIModel): readonly Effort[] {
|
|
377
|
+
if (model.variant === "codex-mini" && semverEqual(model.version, "5.1")) {
|
|
378
|
+
return GPT_5_1_CODEX_MINI_EFFORTS;
|
|
379
|
+
}
|
|
380
|
+
if (semverGte(model.version, "5.2")) {
|
|
381
|
+
return GPT_5_2_PLUS_EFFORTS;
|
|
382
|
+
}
|
|
383
|
+
return DEFAULT_REASONING_EFFORTS;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function inferGeminiSupportedEfforts(model: GeminiModel): readonly Effort[] {
|
|
387
|
+
if (!semverGte(model.version, "3.0")) {
|
|
388
|
+
return DEFAULT_REASONING_EFFORTS;
|
|
389
|
+
}
|
|
390
|
+
return model.kind === "pro" ? GEMINI_3_PRO_EFFORTS : GEMINI_3_FLASH_EFFORTS;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function inferAnthropicSupportedEfforts<TApi extends Api>(
|
|
394
|
+
parsedModel: AnthropicModel,
|
|
395
|
+
model: ApiModel<TApi>,
|
|
396
|
+
): readonly Effort[] {
|
|
397
|
+
if (
|
|
398
|
+
(model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") &&
|
|
399
|
+
semverGte(parsedModel.version, "4.6")
|
|
400
|
+
) {
|
|
401
|
+
return parsedModel.kind === "opus" ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
|
|
402
|
+
}
|
|
403
|
+
return inferFallbackEfforts(model);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function inferFallbackEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[] {
|
|
407
|
+
if (model.api === "anthropic-messages") {
|
|
408
|
+
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
|
|
409
|
+
}
|
|
410
|
+
if (model.api === "bedrock-converse-stream") {
|
|
411
|
+
return DEFAULT_REASONING_EFFORTS;
|
|
412
|
+
}
|
|
413
|
+
if (model.api === "openai-completions") {
|
|
414
|
+
const compat = resolveOpenAICompat(model as ApiModel<"openai-completions">);
|
|
415
|
+
if (compat.thinkingFormat === "openai" && compat.supportsReasoningEffort) {
|
|
416
|
+
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
|
|
417
|
+
}
|
|
418
|
+
return DEFAULT_REASONING_EFFORTS;
|
|
419
|
+
}
|
|
420
|
+
// OpenAI Responses APIs encode discrete effort levels, including xhigh.
|
|
421
|
+
if (model.api === "openai-responses" || model.api === "openai-codex-responses") {
|
|
422
|
+
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
|
|
423
|
+
}
|
|
424
|
+
return DEFAULT_REASONING_EFFORTS;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function inferThinkingControlMode<TApi extends Api>(
|
|
428
|
+
model: ApiModel<TApi>,
|
|
429
|
+
parsedModel: ParsedModel,
|
|
430
|
+
): ThinkingConfig["mode"] {
|
|
431
|
+
switch (model.api) {
|
|
432
|
+
case "google-generative-ai":
|
|
433
|
+
case "google-gemini-cli":
|
|
434
|
+
case "google-vertex":
|
|
435
|
+
return parsedModel.family === "gemini" &&
|
|
436
|
+
semverGte(parsedModel.version, "3.0") &&
|
|
437
|
+
parsedModel.version.major === 3
|
|
438
|
+
? "google-level"
|
|
439
|
+
: "budget";
|
|
440
|
+
|
|
441
|
+
case "anthropic-messages":
|
|
442
|
+
if (parsedModel.family === "anthropic") {
|
|
443
|
+
if (semverGte(parsedModel.version, "4.6")) {
|
|
444
|
+
return "anthropic-adaptive";
|
|
445
|
+
}
|
|
446
|
+
if (semverGte(parsedModel.version, "4.5")) {
|
|
447
|
+
return "anthropic-budget-effort";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return "budget";
|
|
451
|
+
|
|
452
|
+
case "bedrock-converse-stream":
|
|
453
|
+
if (parsedModel.family === "anthropic") {
|
|
454
|
+
if (semverGte(parsedModel.version, "4.6") && parsedModel.kind === "opus") {
|
|
455
|
+
return "anthropic-adaptive";
|
|
456
|
+
}
|
|
457
|
+
if (semverGte(parsedModel.version, "4.5")) {
|
|
458
|
+
return "anthropic-budget-effort";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return "budget";
|
|
462
|
+
|
|
463
|
+
default:
|
|
464
|
+
return "effort";
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function parseKnownModel(modelId: string): ParsedModel {
|
|
469
|
+
const canonicalId = getCanonicalModelId(modelId);
|
|
470
|
+
return (
|
|
471
|
+
parseGeminiModel(canonicalId) ??
|
|
472
|
+
parseAnthropicModel(canonicalId) ??
|
|
473
|
+
parseOpenAIModel(canonicalId) ?? { family: "unknown", id: canonicalId }
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const GEMINI_SUFFIX = "-preview";
|
|
478
|
+
function parseGeminiModel(modelId: string): GeminiModel | null {
|
|
479
|
+
if (modelId.endsWith(GEMINI_SUFFIX)) {
|
|
480
|
+
modelId = modelId.slice(0, -GEMINI_SUFFIX.length);
|
|
481
|
+
}
|
|
482
|
+
const match = /gemini-(\d+(?:\.\d+){0,2})-(pro|flash)\b/.exec(modelId);
|
|
483
|
+
if (!match) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const version = parseSemVer(match[1]);
|
|
487
|
+
if (!version) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
return { family: "gemini", kind: match[2] as GeminiKind, version };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function parseAnthropicModel(modelId: string): AnthropicModel | null {
|
|
494
|
+
const match = /claude-(opus|sonnet)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
|
|
495
|
+
if (!match) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const version = parseSemVer(match[2]);
|
|
499
|
+
if (!version) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return { family: "anthropic", kind: match[1] as AnthropicKind, version };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function parseOpenAIModel(modelId: string): OpenAIModel | null {
|
|
506
|
+
const match = /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|mini|max|nano))?\b/.exec(modelId);
|
|
507
|
+
if (!match) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const version = parseSemVer(match[1]);
|
|
511
|
+
if (!version) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
return { family: "openai", variant: (match[2] as OpenAIVariant | undefined) ?? "base", version };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function createSemVer(major: number, minor: number, patch = 0): SemVer {
|
|
518
|
+
return { major, minor, patch };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// extend this table if we need anything more than 9.10
|
|
522
|
+
const precomputeTable: Record<string, SemVer> = {};
|
|
523
|
+
for (let major = 0; major <= 9; major++) {
|
|
524
|
+
for (let minor = 0; minor <= 10; minor++) {
|
|
525
|
+
const version = createSemVer(major, minor, 0);
|
|
526
|
+
precomputeTable[`${major}.${minor}`] = version;
|
|
527
|
+
precomputeTable[`${major}-${minor}`] = version;
|
|
528
|
+
}
|
|
529
|
+
precomputeTable[`${major}`] = createSemVer(major, 0, 0);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function parseSemVer(version: string): SemVer | null {
|
|
533
|
+
return precomputeTable[version] ?? null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function semverGte(left: SemVer | string, right: SemVer | string): boolean {
|
|
537
|
+
return compareSemVer(left, right) >= 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function semverEqual(left: SemVer | string, right: SemVer | string): boolean {
|
|
541
|
+
return compareSemVer(left, right) === 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function compareSemVer(left: SemVer | string | null, right: SemVer | string | null): number {
|
|
545
|
+
left = typeof left === "string" ? parseSemVer(left) : left;
|
|
546
|
+
right = typeof right === "string" ? parseSemVer(right) : right;
|
|
547
|
+
if (!left || !right) return (left ? 1 : 0) - (right ? 1 : 0);
|
|
548
|
+
|
|
549
|
+
if (left.major !== right.major) {
|
|
550
|
+
return left.major - right.major;
|
|
551
|
+
}
|
|
552
|
+
if (left.minor !== right.minor) {
|
|
553
|
+
return left.minor - right.minor;
|
|
554
|
+
}
|
|
555
|
+
return left.patch - right.patch;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getCanonicalModelId(modelId: string): string {
|
|
559
|
+
const p = modelId.lastIndexOf("/");
|
|
560
|
+
return p !== -1 ? modelId.slice(p + 1) : modelId;
|
|
561
|
+
}
|