@oh-my-pi/pi-catalog 17.2.14 → 17.3.0
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 +29 -0
- package/dist/types/discovery/openai-compatible.d.ts +16 -1
- package/dist/types/identity/family.d.ts +9 -3
- package/dist/types/models.d.ts +2 -0
- package/dist/types/provider-models/openai-compat.d.ts +29 -0
- package/dist/types/types.d.ts +22 -17
- package/package.json +4 -4
- package/src/build.ts +3 -3
- package/src/compat/openai.ts +0 -6
- package/src/discovery/openai-compatible.ts +21 -4
- package/src/identity/classify.ts +29 -6
- package/src/identity/family.ts +18 -4
- package/src/model-manager.ts +13 -2
- package/src/model-thinking.ts +15 -7
- package/src/models.json +2495 -298
- package/src/models.ts +28 -14
- package/src/provider-models/bundled-references.ts +4 -2
- package/src/provider-models/openai-compat.ts +83 -1
- package/src/types.ts +24 -16
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.0] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed `OpenAICompat.enableGeminiThinkingLoopGuard`; thinking-loop eligibility is derived solely from the `model.id` family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added first-party OpenAI Daybreak Blue, Daybreak Red, and GPT-5.6 Cyber models with full support for their documented API pricing (including long-context rates above 272K input), token limits, tools, and reasoning effort controls (off/low/medium/high/xhigh/max).
|
|
14
|
+
- Added calculateUncachedInputCost() to calculate prompt pricing against active context-length tiers without prompt caching.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed Anthropic cache-write pricing to correctly honor mixed 5-minute and 1-hour TTL usage instead of incorrectly charging all writes at the 5-minute rate.
|
|
19
|
+
- Fixed Ollama Cloud DeepSeek V4 Flash and older reasoners to correctly apply the DeepSeek effort contract (e.g., low/high/max) instead of the generic effort ladder.
|
|
20
|
+
- Added a default request timeout to OpenAI-compatible model discovery to prevent stalled provider endpoints from hanging startup indefinitely.
|
|
21
|
+
- Fixed Anthropic cache-write pricing to honor mixed 5-minute and 1-hour TTL usage instead of charging every write at the 5-minute rate.
|
|
22
|
+
- Fixed Ollama Cloud DeepSeek V4 Flash (including dated/preview tags like `deepseek-v4-flash:0731`) exposing the generic `minimal`/`low`/`medium`/`high`/`xhigh` effort ladder without `max`; the `ollama-chat` transport now applies the DeepSeek effort contract (Flash → `low`/`high`/`max`, older reasoners → `high`/`max`), matching the direct API and every other host ([#8334](https://github.com/can1357/oh-my-pi/issues/8334)).
|
|
23
|
+
- Exposed the `low` reasoning-effort tier for DeepSeek V4 Pro on the direct API and faithful aggregator routes, matching DeepSeek's updated API contract advertising `reasoning_effort` `low`/`high`/`max` for both V4 SKUs; OpenRouter's non-Flash route still exposes only `high`, and the older V3.x/R1 reasoners remain `high`/`max` ([#8405](https://github.com/can1357/oh-my-pi/issues/8405)).
|
|
24
|
+
- Bounded OpenAI-compatible model discovery with a default request timeout so a stalled provider `/models` endpoint can no longer hang startup indefinitely in `resolveModelDiscoveryFallback` ([#8315](https://github.com/can1357/oh-my-pi/issues/8315)).
|
|
25
|
+
- Fixed Codex-discovered `gpt-daybreak-*` aliases being treated as unknown models, restoring the GPT-5.6 `low`/`medium`/`high`/`xhigh`/`max` effort ladder and its 372K fallback only when the Codex registry omits `context_window`.
|
|
26
|
+
- Fixed first-party OpenAI GPT-5.6 aliases to preserve wire-level `off` through generated pro aliases and to price requests above 272K input at each SKU's documented long-context rates.
|
|
27
|
+
|
|
28
|
+
## [17.2.15] - 2026-08-12
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed classification of model IDs with large minor versions (e.g., `claude-opus-5-11`) or three-part versions, ensuring they no longer fall back to stale default configurations.
|
|
33
|
+
|
|
5
34
|
## [17.2.13] - 2026-08-11
|
|
6
35
|
|
|
7
36
|
### Changed
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { Api, FetchImpl, ModelSpec, Provider } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default hard deadline applied to an OpenAI-compatible `/models` probe when
|
|
4
|
+
* the caller supplies neither an `AbortSignal` nor an explicit `timeoutMs`.
|
|
5
|
+
*
|
|
6
|
+
* Built-in provider model managers (openrouter, xAI, DeepSeek, …) call
|
|
7
|
+
* {@link fetchOpenAICompatibleModels} with no timeout, so without this bound a
|
|
8
|
+
* stalled endpoint left the request pending forever and blocked startup's
|
|
9
|
+
* awaited `resolveModelDiscoveryFallback` discovery pass indefinitely
|
|
10
|
+
* (issue #8315). 10s matches the coding-agent's remote-discovery budget.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS = 10000;
|
|
2
13
|
/**
|
|
3
14
|
* Minimal OpenAI-style model entry shape consumed by discovery.
|
|
4
15
|
*
|
|
@@ -50,7 +61,11 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
|
|
|
50
61
|
headers?: Record<string, string>;
|
|
51
62
|
/** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
|
|
52
63
|
signal?: AbortSignal;
|
|
53
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Optional cancellable request timeout used when `signal` is omitted.
|
|
66
|
+
* Defaults to {@link DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS} so a
|
|
67
|
+
* stalled endpoint can never hang discovery indefinitely.
|
|
68
|
+
*/
|
|
54
69
|
timeoutMs?: number;
|
|
55
70
|
/** Optional fetch implementation override for testing/custom runtimes. */
|
|
56
71
|
fetch?: FetchImpl;
|
|
@@ -36,13 +36,19 @@ export declare const isGemmaModelId: (modelId: string) => boolean;
|
|
|
36
36
|
export declare const isDeepseekModelIdOrName: (modelId: string) => boolean;
|
|
37
37
|
/**
|
|
38
38
|
* DeepSeek V4 Flash SKU in any host/namespace form (`deepseek-v4-flash`, dated
|
|
39
|
-
* `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`).
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Both V4 SKUs
|
|
40
|
+
* (Flash and Pro) accept the `low` reasoning_effort tier; this predicate keeps
|
|
41
|
+
* Flash distinguishable from Pro where a host quirk splits them (e.g.
|
|
42
|
+
* OpenRouter exposes `low` on Flash but only `high` on non-Flash V4).
|
|
43
|
+
* See https://api-docs.deepseek.com/api/create-chat-completion.
|
|
42
44
|
*/
|
|
43
45
|
export declare const isDeepseekV4FlashModelId: (modelId: string) => boolean;
|
|
44
46
|
/** Xiaomi MiMo family by id or display name. */
|
|
45
47
|
export declare const isMimoModelIdOrName: (modelId: string) => boolean;
|
|
48
|
+
/** Gemini family ids in any namespace form (`gemini-*`, `google/gemini-*`, `openrouter/google/gemini-…`). */
|
|
49
|
+
export declare const isGeminiModelId: (modelId: string) => boolean;
|
|
50
|
+
/** Grok family ids across namespace and delimiter forms (`grok-*`, `cursor-grok-*`, `xai/grok-*`). */
|
|
51
|
+
export declare const isGrokModelId: (modelId: string) => boolean;
|
|
46
52
|
/**
|
|
47
53
|
* Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
|
|
48
54
|
* (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
|
package/dist/types/models.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type GeneratedProvider = keyof typeof MODELS;
|
|
|
4
4
|
export declare function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi>;
|
|
5
5
|
export declare function getBundledProviders(): KnownProvider[];
|
|
6
6
|
export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
|
|
7
|
+
/** Price a prompt as fully uncached input under its active context-length tier. */
|
|
8
|
+
export declare function calculateUncachedInputCost(cost: Model["cost"], promptInputTokens: number): number;
|
|
7
9
|
export declare function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"];
|
|
8
10
|
/**
|
|
9
11
|
* Check if two models are equal by comparing both their id and provider.
|
|
@@ -61,12 +61,41 @@ export interface UmansModelManagerConfig {
|
|
|
61
61
|
fetch?: FetchImpl;
|
|
62
62
|
}
|
|
63
63
|
export declare function umansModelManagerOptions(config?: UmansModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
|
|
64
|
+
export declare const OPENAI_GPT_56_LONG_CONTEXT_COSTS: {
|
|
65
|
+
readonly luna: {
|
|
66
|
+
readonly inputThreshold: 272000;
|
|
67
|
+
readonly input: 0.4;
|
|
68
|
+
readonly output: 1.8;
|
|
69
|
+
readonly cacheRead: 0.04;
|
|
70
|
+
readonly cacheWrite: 0.5;
|
|
71
|
+
};
|
|
72
|
+
readonly sol: {
|
|
73
|
+
readonly inputThreshold: 272000;
|
|
74
|
+
readonly input: 10;
|
|
75
|
+
readonly output: 45;
|
|
76
|
+
readonly cacheRead: 1;
|
|
77
|
+
readonly cacheWrite: 12.5;
|
|
78
|
+
};
|
|
79
|
+
readonly terra: {
|
|
80
|
+
readonly inputThreshold: 272000;
|
|
81
|
+
readonly input: 4;
|
|
82
|
+
readonly output: 18;
|
|
83
|
+
readonly cacheRead: 0.4;
|
|
84
|
+
readonly cacheWrite: 5;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
64
87
|
export interface OpenAIModelManagerConfig {
|
|
65
88
|
apiKey?: string;
|
|
66
89
|
baseUrl?: string;
|
|
67
90
|
fetch?: FetchImpl;
|
|
68
91
|
}
|
|
69
92
|
export declare function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses">;
|
|
93
|
+
/**
|
|
94
|
+
* Daybreak models are approval-gated first-party Responses models that are not
|
|
95
|
+
* yet present in stencil.so. Seed the documented aliases and current Cyber
|
|
96
|
+
* snapshot so fresh installs expose them without credentialed discovery.
|
|
97
|
+
*/
|
|
98
|
+
export declare const OPENAI_DAYBREAK_CURATED_FALLBACK_MODELS: readonly ModelSpec<"openai-responses">[];
|
|
70
99
|
/**
|
|
71
100
|
* Re-derive the generated pro-reasoning aliases (`gpt-5.6-*-pro`) for the
|
|
72
101
|
* first-party `openai` gpt-5.6 rows. Each alias inherits the base row's
|
package/dist/types/types.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ export interface Usage {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "kimi" | "qwen" | "qwen-chat-template";
|
|
125
|
-
export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false";
|
|
125
|
+
export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "none-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false";
|
|
126
126
|
export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
|
|
127
127
|
/**
|
|
128
128
|
* Compatibility settings for openai-completions API.
|
|
@@ -152,13 +152,6 @@ export interface OpenAICompat {
|
|
|
152
152
|
reasoningEffortMap?: Partial<Record<Effort, string>>;
|
|
153
153
|
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
|
|
154
154
|
supportsUsageInStreaming?: boolean;
|
|
155
|
-
/**
|
|
156
|
-
* Enable the Gemini thinking-loop guard (pi-ai stream layer) for this model.
|
|
157
|
-
* Defaults to true when the model id classifies as the gemini family. Set
|
|
158
|
-
* explicitly to cover an opaque OpenAI-compat proxy alias (e.g. `my-model`)
|
|
159
|
-
* that routes to Gemini, or to false to opt a gemini-family id out.
|
|
160
|
-
*/
|
|
161
|
-
enableGeminiThinkingLoopGuard?: boolean;
|
|
162
155
|
/** Which field to use for max tokens. Default: auto-detected from URL. */
|
|
163
156
|
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
|
164
157
|
/** Whether tool results require the `name` field. Default: auto-detected from URL. */
|
|
@@ -582,8 +575,6 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
582
575
|
isOpenRouterHost: boolean;
|
|
583
576
|
/** Whether this endpoint needs a max-token field even when caller did not set one. */
|
|
584
577
|
alwaysSendMaxTokens: boolean;
|
|
585
|
-
/** See {@link OpenAICompat.enableGeminiThinkingLoopGuard}. Set by the builder from the family classifier. */
|
|
586
|
-
enableGeminiThinkingLoopGuard?: boolean;
|
|
587
578
|
openRouterRouting?: OpenAICompat["openRouterRouting"];
|
|
588
579
|
/** Provider-specific wire model-id transform applied to the base id. */
|
|
589
580
|
wireModelIdMode: "raw" | "firepass" | "fireworks" | "openrouter";
|
|
@@ -596,7 +587,7 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
596
587
|
* `buildModel`; request handlers read fields and never detect, resolve, or
|
|
597
588
|
* allocate.
|
|
598
589
|
*/
|
|
599
|
-
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "
|
|
590
|
+
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "whenThinking">> & {
|
|
600
591
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
601
592
|
extraBody?: OpenAICompat["extraBody"];
|
|
602
593
|
cacheControlFormat?: OpenAICompat["cacheControlFormat"];
|
|
@@ -681,6 +672,25 @@ export interface RemoteCompactionConfig<TApi extends Api = Api> {
|
|
|
681
672
|
/** Model id sent to the compaction endpoint when it differs from the active model id. */
|
|
682
673
|
model?: string;
|
|
683
674
|
}
|
|
675
|
+
/** Per-million-token rates for one model pricing tier. */
|
|
676
|
+
export interface TokenCost {
|
|
677
|
+
input: number;
|
|
678
|
+
output: number;
|
|
679
|
+
cacheRead: number;
|
|
680
|
+
cacheWrite: number;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Rates applied to the full request when its prompt exceeds `inputThreshold`.
|
|
684
|
+
* Prompt input is the sum of uncached, cached-read, cache-write, and
|
|
685
|
+
* provider-orchestration input tokens.
|
|
686
|
+
*/
|
|
687
|
+
export interface LongContextTokenCost extends TokenCost {
|
|
688
|
+
inputThreshold: number;
|
|
689
|
+
}
|
|
690
|
+
/** Base token rates plus an optional long-context tier. */
|
|
691
|
+
export interface ModelCost extends TokenCost {
|
|
692
|
+
longContext?: LongContextTokenCost;
|
|
693
|
+
}
|
|
684
694
|
export interface Model<TApi extends Api = Api> {
|
|
685
695
|
id: string;
|
|
686
696
|
/**
|
|
@@ -725,12 +735,7 @@ export interface Model<TApi extends Api = Api> {
|
|
|
725
735
|
gitlabDuoWorkflowRootNamespaceId?: string;
|
|
726
736
|
/** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
|
|
727
737
|
cursorMaxMode?: boolean;
|
|
728
|
-
cost:
|
|
729
|
-
input: number;
|
|
730
|
-
output: number;
|
|
731
|
-
cacheRead: number;
|
|
732
|
-
cacheWrite: number;
|
|
733
|
-
};
|
|
738
|
+
cost: ModelCost;
|
|
734
739
|
/** Premium Copilot requests charged per user-initiated request (defaults to 1). */
|
|
735
740
|
premiumMultiplier?: number;
|
|
736
741
|
contextWindow: number | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-catalog",
|
|
4
|
-
"version": "17.
|
|
4
|
+
"version": "17.3.0",
|
|
5
5
|
"description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@bufbuild/protobuf": "^2.12.1",
|
|
38
|
-
"@oh-my-pi/omptype": "17.
|
|
39
|
-
"@oh-my-pi/pi-utils": "17.
|
|
38
|
+
"@oh-my-pi/omptype": "17.3.0",
|
|
39
|
+
"@oh-my-pi/pi-utils": "17.3.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.3.0",
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
package/src/build.ts
CHANGED
|
@@ -14,12 +14,11 @@ import { buildAnthropicCompat } from "./compat/anthropic";
|
|
|
14
14
|
import { buildBedrockCompat } from "./compat/bedrock";
|
|
15
15
|
import { buildDevinCompat } from "./compat/devin";
|
|
16
16
|
import { buildOpenAICompat, buildOpenAIResponsesCompat, buildOpenRouterCompat } from "./compat/openai";
|
|
17
|
+
import { bareModelId, parseOpenAIModel, semverGte } from "./identity/classify";
|
|
17
18
|
import { resolveModelThinking } from "./model-thinking";
|
|
18
19
|
import type { Api, CompatOf, Model, ModelSpec } from "./types";
|
|
19
20
|
import { cleanModelName } from "./utils";
|
|
20
21
|
|
|
21
|
-
const OPENAI_GA_COMPUTER_MODEL_RE = /^gpt-5\.(?:[4-9]|[1-9]\d)(?:[.-]|$)/i;
|
|
22
|
-
|
|
23
22
|
function isDirectOpenAIResponsesEndpoint(spec: ModelSpec<Api>): boolean {
|
|
24
23
|
if (spec.api === "openai-responses") {
|
|
25
24
|
if (spec.provider !== "openai") return false;
|
|
@@ -55,7 +54,8 @@ function explicitComputerUseConfig(spec: ModelSpec<Api>): boolean | undefined {
|
|
|
55
54
|
function supportsOpenAIGAComputerUse(spec: ModelSpec<Api>, explicitSupport: boolean | undefined): boolean {
|
|
56
55
|
if (explicitSupport !== undefined) return explicitSupport;
|
|
57
56
|
if (!isDirectOpenAIResponsesEndpoint(spec)) return false;
|
|
58
|
-
|
|
57
|
+
const parsed = parseOpenAIModel(bareModelId(spec.requestModelId ?? spec.id));
|
|
58
|
+
return parsed !== null && semverGte(parsed.version, "5.4");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi> {
|
package/src/compat/openai.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
isMimoModelIdOrName,
|
|
23
23
|
isOpenAISamplingRestrictedModelId,
|
|
24
24
|
isQwenModelId,
|
|
25
|
-
modelFamilyToken,
|
|
26
25
|
} from "../identity/family";
|
|
27
26
|
import type {
|
|
28
27
|
ModelSpec,
|
|
@@ -474,10 +473,6 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
474
473
|
supportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),
|
|
475
474
|
reasoningEffortMap: {},
|
|
476
475
|
supportsUsageInStreaming: !isCerebras,
|
|
477
|
-
// pi-ai's thinking-loop guard is gemini-only; default the flag from the
|
|
478
|
-
// family classifier so OpenAI-compat proxies serving Gemini are covered.
|
|
479
|
-
// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.
|
|
480
|
-
enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === "gemini",
|
|
481
476
|
// Kimi (including via OpenRouter and Fireworks router-form IDs such as
|
|
482
477
|
// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on
|
|
483
478
|
// max_tokens, not actual output. The official Kimi K2 model guidance
|
|
@@ -749,7 +744,6 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
749
744
|
// lands on Moonshot's MFJS validator.
|
|
750
745
|
toolSchemaFlavor: isKimiModel ? "moonshot-mfjs" : undefined,
|
|
751
746
|
alwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,
|
|
752
|
-
enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? "") === "gemini",
|
|
753
747
|
supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
|
|
754
748
|
stripDeepseekSpecialTokens:
|
|
755
749
|
Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
|
|
@@ -4,6 +4,18 @@ import { discoveryFetch } from "../utils";
|
|
|
4
4
|
|
|
5
5
|
const MODELS_PATH = "/models";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Default hard deadline applied to an OpenAI-compatible `/models` probe when
|
|
9
|
+
* the caller supplies neither an `AbortSignal` nor an explicit `timeoutMs`.
|
|
10
|
+
*
|
|
11
|
+
* Built-in provider model managers (openrouter, xAI, DeepSeek, …) call
|
|
12
|
+
* {@link fetchOpenAICompatibleModels} with no timeout, so without this bound a
|
|
13
|
+
* stalled endpoint left the request pending forever and blocked startup's
|
|
14
|
+
* awaited `resolveModelDiscoveryFallback` discovery pass indefinitely
|
|
15
|
+
* (issue #8315). 10s matches the coding-agent's remote-discovery budget.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* Uses a cancellable timer rather than the native abort-timeout helper so
|
|
9
21
|
* successful fast discovery requests do not leave armed timeout signals for
|
|
@@ -96,7 +108,11 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
|
|
|
96
108
|
headers?: Record<string, string>;
|
|
97
109
|
/** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
|
|
98
110
|
signal?: AbortSignal;
|
|
99
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Optional cancellable request timeout used when `signal` is omitted.
|
|
113
|
+
* Defaults to {@link DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS} so a
|
|
114
|
+
* stalled endpoint can never hang discovery indefinitely.
|
|
115
|
+
*/
|
|
100
116
|
timeoutMs?: number;
|
|
101
117
|
/** Optional fetch implementation override for testing/custom runtimes. */
|
|
102
118
|
fetch?: FetchImpl;
|
|
@@ -164,9 +180,10 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
164
180
|
const payload =
|
|
165
181
|
options.signal !== undefined
|
|
166
182
|
? await fetchPayload(options.signal)
|
|
167
|
-
:
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
: await withOpenAICompatibleDiscoveryTimeout(
|
|
184
|
+
options.timeoutMs ?? DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
|
|
185
|
+
fetchPayload,
|
|
186
|
+
);
|
|
170
187
|
if (payload === null) {
|
|
171
188
|
return null;
|
|
172
189
|
}
|
package/src/identity/classify.ts
CHANGED
|
@@ -122,16 +122,31 @@ export const parseAnthropicModel = parser((modelId): AnthropicModel | null => {
|
|
|
122
122
|
return { family: "anthropic", kind: kind as AnthropicKind, version };
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Rolling OpenAI aliases inherit wire capabilities from their current default
|
|
127
|
+
* snapshots. Keep this map aligned with the model docs when an alias advances.
|
|
128
|
+
*/
|
|
129
|
+
const OPENAI_ALIAS_VERSIONS: Readonly<Record<string, string>> = {
|
|
130
|
+
"daybreak-blue-latest": "5.6",
|
|
131
|
+
"gpt-daybreak-blue-latest": "5.6",
|
|
132
|
+
"daybreak-red-latest": "5.6",
|
|
133
|
+
"gpt-daybreak-red-latest": "5.6",
|
|
134
|
+
};
|
|
135
|
+
|
|
125
136
|
export const parseOpenAIModel = parser((modelId): OpenAIModel | null => {
|
|
126
|
-
const
|
|
127
|
-
|
|
137
|
+
const aliasVersion = OPENAI_ALIAS_VERSIONS[modelId];
|
|
138
|
+
const match = aliasVersion
|
|
139
|
+
? null
|
|
140
|
+
: /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|mini|max|nano))?\b/.exec(modelId);
|
|
141
|
+
const versionInput = aliasVersion ?? match?.[1];
|
|
142
|
+
if (!versionInput) {
|
|
128
143
|
return null;
|
|
129
144
|
}
|
|
130
|
-
const version = parseSemVer(
|
|
145
|
+
const version = parseSemVer(versionInput);
|
|
131
146
|
if (!version) {
|
|
132
147
|
return null;
|
|
133
148
|
}
|
|
134
|
-
return { family: "openai", variant: (match[2] as OpenAIVariant | undefined) ?? "base", version };
|
|
149
|
+
return { family: "openai", variant: (match?.[2] as OpenAIVariant | undefined) ?? "base", version };
|
|
135
150
|
});
|
|
136
151
|
|
|
137
152
|
/**
|
|
@@ -181,7 +196,9 @@ function createSemVer(major: number, minor: number, patch = 0): SemVer {
|
|
|
181
196
|
return { major, minor, patch };
|
|
182
197
|
}
|
|
183
198
|
|
|
184
|
-
//
|
|
199
|
+
// Fast path for the common 1–2 component versions; anything the table misses
|
|
200
|
+
// (large minors, 3-part versions) parses dynamically below so no future
|
|
201
|
+
// version ever classifies as unknown (the failure class #8256 fixed).
|
|
185
202
|
const precomputeTable: Record<string, SemVer> = {};
|
|
186
203
|
for (let major = 0; major <= 9; major++) {
|
|
187
204
|
for (let minor = 0; minor <= 10; minor++) {
|
|
@@ -192,8 +209,14 @@ for (let major = 0; major <= 9; major++) {
|
|
|
192
209
|
precomputeTable[`${major}`] = createSemVer(major, 0, 0);
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
const SEMVER_PATTERN = /^(\d{1,2})(?:[.-](\d{1,2}))?(?:[.-](\d{1,2}))?$/;
|
|
213
|
+
|
|
195
214
|
export function parseSemVer(version: string): SemVer | null {
|
|
196
|
-
|
|
215
|
+
const hit = precomputeTable[version];
|
|
216
|
+
if (hit) return hit;
|
|
217
|
+
const match = SEMVER_PATTERN.exec(version);
|
|
218
|
+
if (!match) return null;
|
|
219
|
+
return createSemVer(Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0));
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
export function semverGte(left: SemVer | string, right: SemVer | string): boolean {
|
package/src/identity/family.ts
CHANGED
|
@@ -85,9 +85,11 @@ export const isDeepseekModelIdOrName = memo((value: string): boolean => {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* DeepSeek V4 Flash SKU in any host/namespace form (`deepseek-v4-flash`, dated
|
|
88
|
-
* `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`).
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Both V4 SKUs
|
|
89
|
+
* (Flash and Pro) accept the `low` reasoning_effort tier; this predicate keeps
|
|
90
|
+
* Flash distinguishable from Pro where a host quirk splits them (e.g.
|
|
91
|
+
* OpenRouter exposes `low` on Flash but only `high` on non-Flash V4).
|
|
92
|
+
* See https://api-docs.deepseek.com/api/create-chat-completion.
|
|
91
93
|
*/
|
|
92
94
|
export const isDeepseekV4FlashModelId = memo((modelId: string): boolean => {
|
|
93
95
|
return bareModelId(modelId).toLowerCase().includes("deepseek-v4-flash");
|
|
@@ -98,6 +100,16 @@ export const isMimoModelIdOrName = memo((value: string): boolean => {
|
|
|
98
100
|
return value.toLowerCase().includes("mimo");
|
|
99
101
|
});
|
|
100
102
|
|
|
103
|
+
/** Gemini family ids in any namespace form (`gemini-*`, `google/gemini-*`, `openrouter/google/gemini-…`). */
|
|
104
|
+
export const isGeminiModelId = memo((modelId: string): boolean => {
|
|
105
|
+
return /(^|\/)gemini[-.]?/i.test(modelId);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** Grok family ids across namespace and delimiter forms (`grok-*`, `cursor-grok-*`, `xai/grok-*`). */
|
|
109
|
+
export const isGrokModelId = memo((modelId: string): boolean => {
|
|
110
|
+
return /(?:^|[./_-])grok(?:[-.]|$)/i.test(modelId);
|
|
111
|
+
});
|
|
112
|
+
|
|
101
113
|
const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3", "grok-4.5"] as const;
|
|
102
114
|
|
|
103
115
|
/**
|
|
@@ -259,12 +271,14 @@ export const modelFamilyToken = memo((modelId: string): string => {
|
|
|
259
271
|
const parsed = parseKnownModel(modelId);
|
|
260
272
|
if (parsed.family !== "unknown") return parsed.family;
|
|
261
273
|
if (isClaudeModelId(modelId) || isAnthropicNamespacedModelId(modelId)) return "anthropic";
|
|
274
|
+
if (isGeminiModelId(modelId)) return "gemini";
|
|
275
|
+
if (isGrokModelId(modelId)) return "grok";
|
|
276
|
+
if (isDeepseekModelIdOrName(modelId)) return "deepseek";
|
|
262
277
|
if (isOpenAIModelId(modelId)) return "openai";
|
|
263
278
|
if (isKimiModelId(modelId)) return "kimi";
|
|
264
279
|
if (isQwenModelId(modelId)) return "qwen";
|
|
265
280
|
if (isMinimaxM2FamilyModelId(modelId) || isMinimaxM3FamilyModelId(modelId)) return "minimax";
|
|
266
281
|
if (isOpenAIGptOssModelId(modelId)) return "gpt-oss";
|
|
267
|
-
if (isDeepseekModelIdOrName(modelId)) return "deepseek";
|
|
268
282
|
if (isMimoModelIdOrName(modelId)) return "mimo";
|
|
269
283
|
if (isGemmaModelId(modelId)) return "gemma";
|
|
270
284
|
if (parseGlmModel(bareModelId(modelId))) return "glm";
|
package/src/model-manager.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildModel } from "./build";
|
|
2
2
|
import { readModelCache, writeModelCache } from "./model-cache";
|
|
3
3
|
import { type GeneratedProvider, getBundledModels } from "./models";
|
|
4
|
-
import type { Api, Model, ModelSpec, Provider } from "./types";
|
|
4
|
+
import type { Api, Model, ModelCost, ModelSpec, Provider, TokenCost } from "./types";
|
|
5
5
|
import { isRecord } from "./utils";
|
|
6
6
|
import { collapseBuiltModelVariants } from "./variant-collapse";
|
|
7
7
|
|
|
@@ -510,6 +510,7 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
510
510
|
const reasoning = dynamicReasoningAuthoritative
|
|
511
511
|
? dynamicModel.reasoning
|
|
512
512
|
: existingModel.reasoning || dynamicModel.reasoning;
|
|
513
|
+
const longContextCost = dynamicModel.cost.longContext ?? existingModel.cost.longContext;
|
|
513
514
|
// Re-build from spec stage: sparse compat comes from `compatConfig` (the
|
|
514
515
|
// verbatim override vocabulary), never the resolved `compat` record.
|
|
515
516
|
return buildModel({
|
|
@@ -523,6 +524,7 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
523
524
|
output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output),
|
|
524
525
|
cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
|
|
525
526
|
cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
|
|
527
|
+
...(longContextCost ? { longContext: longContextCost } : {}),
|
|
526
528
|
},
|
|
527
529
|
contextWindow: preferDiscoveryLimit(dynamicModel.contextWindow, existingModel.contextWindow),
|
|
528
530
|
maxTokens: preferDiscoveryLimit(dynamicModel.maxTokens, existingModel.maxTokens),
|
|
@@ -640,7 +642,7 @@ function isModelInputArray(value: unknown): value is ("text" | "image")[] {
|
|
|
640
642
|
return true;
|
|
641
643
|
}
|
|
642
644
|
|
|
643
|
-
function
|
|
645
|
+
function isTokenCost(value: unknown): value is TokenCost {
|
|
644
646
|
if (!isRecord(value)) {
|
|
645
647
|
return false;
|
|
646
648
|
}
|
|
@@ -670,3 +672,12 @@ function isModelCost(value: unknown): value is Model<Api>["cost"] {
|
|
|
670
672
|
}
|
|
671
673
|
return true;
|
|
672
674
|
}
|
|
675
|
+
|
|
676
|
+
function isModelCost(value: unknown): value is ModelCost {
|
|
677
|
+
if (!isTokenCost(value)) return false;
|
|
678
|
+
const longContext = (value as TokenCost & { longContext?: unknown }).longContext;
|
|
679
|
+
if (longContext === undefined) return true;
|
|
680
|
+
if (!isTokenCost(longContext) || !isRecord(longContext)) return false;
|
|
681
|
+
const threshold = longContext.inputThreshold;
|
|
682
|
+
return typeof threshold === "number" && threshold > 0 && threshold < Infinity;
|
|
683
|
+
}
|
package/src/model-thinking.ts
CHANGED
|
@@ -63,9 +63,9 @@ const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, E
|
|
|
63
63
|
const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
64
64
|
const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High];
|
|
65
65
|
const LOW_MEDIUM_HIGH_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
|
|
66
|
-
/** Wire-exact `low`/`high`/`max` scale used by Kimi K3 and DeepSeek V4 Flash
|
|
66
|
+
/** Wire-exact `low`/`high`/`max` scale used by Kimi K3 and DeepSeek V4 (Flash and Pro, direct API and aggregators). */
|
|
67
67
|
const LOW_HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max];
|
|
68
|
-
/** Wire-exact two-tier scale (`high`/`max`): GLM-5.2 on Z.ai/Umans/Ollama Cloud/Baseten, Sakana Fugu, DeepSeek
|
|
68
|
+
/** Wire-exact two-tier scale (`high`/`max`): GLM-5.2 on Z.ai/Umans/Ollama Cloud/Baseten, Sakana Fugu, older DeepSeek reasoners (V3.x/R1). */
|
|
69
69
|
const HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.High, Effort.Max];
|
|
70
70
|
/** OpenRouter's DeepSeek route accepts only `high`. */
|
|
71
71
|
const HIGH_ONLY_REASONING_EFFORTS: readonly Effort[] = [Effort.High];
|
|
@@ -366,14 +366,22 @@ function getModelDefinedEfforts<TApi extends Api>(
|
|
|
366
366
|
if (spec.provider === "ollama") {
|
|
367
367
|
return OLLAMA_REASONING_EFFORTS;
|
|
368
368
|
}
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
//
|
|
369
|
+
if (
|
|
370
|
+
(isOpenAICompatReasoningApi(spec.api) || (spec.api === "ollama-chat" && spec.provider === "ollama-cloud")) &&
|
|
371
|
+
isDeepseekReasoningModel(spec)
|
|
372
|
+
) {
|
|
373
|
+
// DeepSeek V4 (Flash and Pro) accepts the wire-exact low/high/max ladder
|
|
374
|
+
// on every first-party/aggregator host — the direct API, aggregators, and
|
|
375
|
+
// Ollama Cloud alike (medium/xhigh fold into high, max is a real wire
|
|
376
|
+
// tier). See https://api-docs.deepseek.com/api/create-chat-completion.
|
|
377
|
+
// OpenRouter's non-Flash V4 route still exposes only high; the older
|
|
378
|
+
// reasoners (V3.x, R1, deepseek-reasoner) top out at high/max.
|
|
374
379
|
if (isDeepseekV4FlashModelId(spec.id)) {
|
|
375
380
|
return LOW_HIGH_MAX_REASONING_EFFORTS;
|
|
376
381
|
}
|
|
382
|
+
if (bareModelId(spec.id).toLowerCase().includes("deepseek-v4")) {
|
|
383
|
+
return isOpenRouterThinkingFormat(compat) ? HIGH_ONLY_REASONING_EFFORTS : LOW_HIGH_MAX_REASONING_EFFORTS;
|
|
384
|
+
}
|
|
377
385
|
return isOpenRouterThinkingFormat(compat) ? HIGH_ONLY_REASONING_EFFORTS : HIGH_MAX_REASONING_EFFORTS;
|
|
378
386
|
}
|
|
379
387
|
if (spec.provider === "baseten" && isOpenAIGptOssModelId(spec.id)) {
|