@oh-my-pi/pi-catalog 17.2.1 → 17.2.3
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 +25 -0
- package/README.md +13 -13
- package/dist/types/hosts.d.ts +5 -0
- package/dist/types/model-manager.d.ts +5 -5
- package/dist/types/provider-models/descriptors.d.ts +18 -0
- package/dist/types/provider-models/openai-compat.d.ts +48 -0
- package/dist/types/types.d.ts +2 -0
- package/package.json +3 -3
- package/src/compat/openai.ts +4 -0
- package/src/discovery/cursor.ts +59 -2
- package/src/hosts.ts +2 -0
- package/src/model-manager.ts +34 -8
- package/src/models.json +6600 -637
- package/src/models.ts +1 -1
- package/src/provider-models/cache-provider-id.ts +3 -1
- package/src/provider-models/descriptors.ts +18 -0
- package/src/provider-models/openai-compat.ts +435 -19
- package/src/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fixed `gen:models` Codex discovery to union models across every stored OAuth account and fail closed on partial resolution, matching runtime discovery ([#6265](https://github.com/can1357/oh-my-pi/issues/6265)); restored the bundled `gpt-5.4`, `gpt-5.6-sol`, and `gpt-5.3-codex-spark` entries a single-account regen had dropped.
|
|
8
|
+
|
|
9
|
+
## [17.2.3] - 2026-08-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable.
|
|
14
|
+
|
|
15
|
+
## [17.2.2] - 2026-07-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added support for the GMI Cloud provider (`gmi-cloud`), an OpenAI-compatible inference gateway with dynamic model discovery and API-key authentication via the `GMI_API_KEY` environment variable.
|
|
20
|
+
- Added optional authoritative context occupancy to usage records for providers with separate checkpoint telemetry and billable token buckets.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed classification of dynamically discovered Cursor Kimi K3 effort variants as non-reasoning models when `thinkingDetails` is omitted.
|
|
25
|
+
- Fixed Google AI Studio OpenAI-compatible requests failing with HTTP 400 by omitting the unsupported `store` field.
|
|
26
|
+
- Fixed Synthetic models losing capabilities (such as reasoning/thinking selectors, vision input, output limits, and pricing) by correcting how the discovery mapper parses Synthetic's advertised features, effort vocabularies, and pricing structures.
|
|
27
|
+
- Fixed Cursor model discovery to correctly expose the 1M-token context window for supported models (including Claude, GPT, Kimi K3, and GLM 5.2+ families) instead of defaulting to 200k.
|
|
28
|
+
- Fixed GitHub Copilot routing for `grok-4.5` to use the correct Responses endpoint instead of the unsupported Chat Completions endpoint.
|
|
29
|
+
|
|
5
30
|
## [17.2.1] - 2026-07-30
|
|
6
31
|
|
|
7
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -4,24 +4,24 @@ Model catalog for [oh-my-pi](https://github.com/can1357/oh-my-pi): bundled model
|
|
|
4
4
|
|
|
5
5
|
## What's inside
|
|
6
6
|
|
|
7
|
-
| Module
|
|
8
|
-
|
|
|
9
|
-
| `models.json` + `models`
|
|
10
|
-
| `provider-models`
|
|
11
|
-
| `discovery`
|
|
12
|
-
| `identity`
|
|
13
|
-
| `model-thinking`
|
|
14
|
-
| `model-manager` / `model-cache` | Runtime model registry with discovery refresh and on-disk caching
|
|
15
|
-
| `variant-collapse`
|
|
16
|
-
| `compat`
|
|
17
|
-
| `wire`
|
|
18
|
-
| `effort`
|
|
7
|
+
| Module | Purpose |
|
|
8
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `models.json` + `models` | Bundled model database (pricing, context windows, modalities, thinking support) |
|
|
10
|
+
| `provider-models` | Provider catalog descriptors (`CATALOG_PROVIDERS`), per-provider model resolution rules |
|
|
11
|
+
| `discovery` | Runtime model discovery for OpenAI-compatible endpoints, Gemini, Codex, Cursor, Antigravity, Ollama |
|
|
12
|
+
| `identity` | Model id parsing and classification (family/version), reference resolution, equivalence, selection priority |
|
|
13
|
+
| `model-thinking` | Thinking/reasoning metadata and generated per-model policies |
|
|
14
|
+
| `model-manager` / `model-cache` | Runtime model registry with discovery refresh and on-disk caching |
|
|
15
|
+
| `variant-collapse` | Collapsing provider-specific variants of the same underlying model |
|
|
16
|
+
| `compat` | Request/response compatibility fixups for OpenAI- and Anthropic-shaped APIs |
|
|
17
|
+
| `wire` | Wire-level helpers: Codex, Gemini headers, GitHub Copilot |
|
|
18
|
+
| `effort` | Reasoning-effort level definitions |
|
|
19
19
|
|
|
20
20
|
Import from subpaths (`@oh-my-pi/pi-catalog/<module>`) or the root barrel.
|
|
21
21
|
|
|
22
22
|
## models.json is generated
|
|
23
23
|
|
|
24
|
-
Never edit `src/models.json` by hand — it is produced from upstream sources (
|
|
24
|
+
Never edit `src/models.json` by hand — it is produced from upstream sources (stencil.so, provider catalog discovery, OpenCode docs) by `scripts/generate-models.ts` and the resolvers in `src/provider-models/`. Regenerate with:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
27
|
bun run gen:models
|
package/dist/types/hosts.d.ts
CHANGED
|
@@ -115,6 +115,11 @@ export declare const KNOWN_HOSTS: {
|
|
|
115
115
|
readonly providers: readonly ["moonshot", "kimi-code"];
|
|
116
116
|
readonly urlMarkers: readonly ["api.moonshot.ai", "api.kimi.com"];
|
|
117
117
|
};
|
|
118
|
+
/** Google AI Studio's OpenAI-compatible shim (`/v1beta/openai`) — a subset of chat-completions; rejects `store` with a 400. Native Gemini uses `google-generative-ai` api instead. */
|
|
119
|
+
readonly googleAistudio: {
|
|
120
|
+
readonly providers: readonly [];
|
|
121
|
+
readonly urlMarkers: readonly ["generativelanguage.googleapis.com"];
|
|
122
|
+
};
|
|
118
123
|
readonly opencode: {
|
|
119
124
|
readonly providers: readonly ["opencode-go", "opencode-zen"];
|
|
120
125
|
readonly urlMarkers: readonly ["opencode.ai"];
|
|
@@ -4,10 +4,10 @@ import type { Api, Model, ModelSpec, Provider } from "./types.js";
|
|
|
4
4
|
*/
|
|
5
5
|
export type ModelRefreshStrategy = "online" | "offline" | "online-if-uncached";
|
|
6
6
|
/**
|
|
7
|
-
* Hook for loading and mapping
|
|
7
|
+
* Hook for loading and mapping stencil.so fallback data into canonical model objects.
|
|
8
8
|
*/
|
|
9
9
|
export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
|
|
10
|
-
/** Fetches raw fallback payload (for example from
|
|
10
|
+
/** Fetches raw fallback payload (for example from stencil.so). */
|
|
11
11
|
fetch(): Promise<TPayload>;
|
|
12
12
|
/** Maps payload into provider models. */
|
|
13
13
|
map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
|
|
@@ -28,7 +28,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
28
28
|
cacheTtlMs?: number;
|
|
29
29
|
/** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
|
|
30
30
|
dynamicModelsAuthoritative?: boolean;
|
|
31
|
-
/** Cached model ids
|
|
31
|
+
/** Cached model ids whose presence forces refresh when the static or migration-policy fingerprint changes. */
|
|
32
32
|
dropCachedModelIdsOnStaticMismatch?: readonly string[];
|
|
33
33
|
/**
|
|
34
34
|
* Trusted, provider-wide request headers (compile-time constants, never
|
|
@@ -40,7 +40,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
40
40
|
restorableHeaderFallback?: Record<string, string>;
|
|
41
41
|
/** Optional dynamic endpoint fetcher. */
|
|
42
42
|
fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
|
|
43
|
-
/** Optional
|
|
43
|
+
/** Optional stencil.so fallback hook. */
|
|
44
44
|
modelsDev?: ModelsDevFallback<TApi, TModelsDevPayload>;
|
|
45
45
|
/** Clock override for deterministic tests. */
|
|
46
46
|
now?: () => number;
|
|
@@ -70,7 +70,7 @@ export interface ModelManager<TApi extends Api = Api> {
|
|
|
70
70
|
export declare function createModelManager<TApi extends Api = Api, TModelsDevPayload = unknown>(options: ModelManagerOptions<TApi, TModelsDevPayload>): ModelManager<TApi>;
|
|
71
71
|
/**
|
|
72
72
|
* Resolves provider models with source precedence:
|
|
73
|
-
* static ->
|
|
73
|
+
* static -> stencil.so -> cache -> dynamic.
|
|
74
74
|
*
|
|
75
75
|
* Later sources override earlier ones by model id.
|
|
76
76
|
*/
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ModelManagerConfig, ProviderCatalogEntry, ProviderDescriptor } from "./descriptor-types.js";
|
|
9
9
|
export declare const CATALOG_PROVIDERS: readonly [{
|
|
10
|
+
readonly id: "aiand";
|
|
11
|
+
readonly defaultModel: "moonshotai/kimi-k2.7-code";
|
|
12
|
+
readonly envVars: readonly ["AIAND_API_KEY"];
|
|
13
|
+
readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-completions", unknown>;
|
|
14
|
+
readonly dynamicModelsAuthoritative: true;
|
|
15
|
+
readonly catalogDiscovery: {
|
|
16
|
+
readonly label: "ai&";
|
|
17
|
+
};
|
|
18
|
+
}, {
|
|
10
19
|
readonly id: "aimlapi";
|
|
11
20
|
readonly defaultModel: "gpt-5.5-2026-04-23";
|
|
12
21
|
readonly envVars: readonly ["AIMLAPI_API_KEY"];
|
|
@@ -129,6 +138,15 @@ export declare const CATALOG_PROVIDERS: readonly [{
|
|
|
129
138
|
readonly envVars: readonly ["GITLAB_TOKEN"];
|
|
130
139
|
readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"gitlab-duo-agent", unknown>;
|
|
131
140
|
readonly dynamicModelsAuthoritative: true;
|
|
141
|
+
}, {
|
|
142
|
+
readonly id: "gmi-cloud";
|
|
143
|
+
readonly defaultModel: "deepseek-ai/DeepSeek-V4-Flash";
|
|
144
|
+
readonly envVars: readonly ["GMI_API_KEY"];
|
|
145
|
+
readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-completions", unknown>;
|
|
146
|
+
readonly dynamicModelsAuthoritative: true;
|
|
147
|
+
readonly catalogDiscovery: {
|
|
148
|
+
readonly label: "GMI Cloud";
|
|
149
|
+
};
|
|
132
150
|
}, {
|
|
133
151
|
readonly id: "google";
|
|
134
152
|
readonly defaultModel: "gemini-3.1-pro-preview";
|
|
@@ -25,6 +25,17 @@ export interface ModelsDevModel {
|
|
|
25
25
|
npm?: string;
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetches the models.dev catalog via catalog.stencil.so, which serves a
|
|
30
|
+
* field-pruned copy precompressed as a zstd blob (~93 KB vs ~3.3 MB raw).
|
|
31
|
+
* The frame magic is sniffed rather than trusting content-type so plain-JSON
|
|
32
|
+
* responses (test stubs, fallback mirrors) parse identically.
|
|
33
|
+
*
|
|
34
|
+
* Fetched fully once per process: concurrent callers share the in-flight
|
|
35
|
+
* request, repeat callers send a conditional GET that the server answers
|
|
36
|
+
* (and deliberately does not log) with `304`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown>;
|
|
28
39
|
/**
|
|
29
40
|
* Curated Anthropic models that are live or limited-availability on the
|
|
30
41
|
* first-party `/v1/models` endpoint but that models.dev has not catalogued yet.
|
|
@@ -67,6 +78,26 @@ export declare function openaiModelManagerOptions(config?: OpenAIModelManagerCon
|
|
|
67
78
|
* upstream model that occupies an alias id wins and suppresses the projection.
|
|
68
79
|
*/
|
|
69
80
|
export declare function projectOpenAIProReasoningAliases(models: readonly ModelSpec<Api>[]): ModelSpec<Api>[];
|
|
81
|
+
/**
|
|
82
|
+
* Bundled seed for GMI Cloud. Generation has no `GMI_API_KEY`, so a regen
|
|
83
|
+
* without credentials would leave the provider slice empty and the declared
|
|
84
|
+
* `defaultModel` unresolvable on a fresh install before the async runtime
|
|
85
|
+
* discovery fires. Live `/v1/models` discovery is authoritative for the model
|
|
86
|
+
* ID set and overrides context/max-token limits, but `mapWithBundledReference`
|
|
87
|
+
* keeps the reference's cost/reasoning/thinking — so these fields carry GMI's
|
|
88
|
+
* direct-tariff values: V4-Flash at $0.14/$0.28 per 1M with Think High/Max
|
|
89
|
+
* modes per GMI's launch post
|
|
90
|
+
* (https://www.gmicloud.ai/en/blog/deepseek-v4-is-here-we-tested-it), not
|
|
91
|
+
* discounted gateway-route pricing. GMI publishes no cache-read tariff, so
|
|
92
|
+
* cacheRead stays 0 until a direct source confirms cached-token billing.
|
|
93
|
+
*/
|
|
94
|
+
export declare const GMI_CLOUD_STATIC_MODELS: readonly ModelSpec<"openai-completions">[];
|
|
95
|
+
export interface GmiCloudModelManagerConfig {
|
|
96
|
+
apiKey?: string;
|
|
97
|
+
baseUrl?: string;
|
|
98
|
+
fetch?: FetchImpl;
|
|
99
|
+
}
|
|
100
|
+
export declare function gmiCloudModelManagerOptions(config?: GmiCloudModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
70
101
|
export interface GroqModelManagerConfig {
|
|
71
102
|
apiKey?: string;
|
|
72
103
|
baseUrl?: string;
|
|
@@ -412,6 +443,23 @@ export interface SakanaModelManagerConfig {
|
|
|
412
443
|
fetch?: FetchImpl;
|
|
413
444
|
}
|
|
414
445
|
export declare function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): ModelManagerOptions<"openai-responses">;
|
|
446
|
+
/**
|
|
447
|
+
* Documented ai& catalog (docs.aiand.com/models/catalog, 2026-08) bundled so
|
|
448
|
+
* the provider is usable when generation and first boot have no live key.
|
|
449
|
+
* The org-scoped `/v1/models` response is authoritative once discovery runs.
|
|
450
|
+
*/
|
|
451
|
+
export declare const AIAND_STATIC_MODELS: readonly ModelSpec<"openai-completions">[];
|
|
452
|
+
export interface AiandModelManagerConfig {
|
|
453
|
+
apiKey?: string;
|
|
454
|
+
baseUrl?: string;
|
|
455
|
+
fetch?: FetchImpl;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* ai& (aiand.com) model manager: OpenAI-compatible chat completions with an
|
|
459
|
+
* org-scoped `/v1/models` catalog carrying context, capability, effort, and
|
|
460
|
+
* pricing metadata, so discovery is authoritative over the bundled seed.
|
|
461
|
+
*/
|
|
462
|
+
export declare function aiandModelManagerOptions(config?: AiandModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
415
463
|
export interface QwenPortalModelManagerConfig {
|
|
416
464
|
apiKey?: string;
|
|
417
465
|
baseUrl?: string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface Usage {
|
|
|
75
75
|
cacheWrite: number;
|
|
76
76
|
/** Sum of input + output + cacheRead + cacheWrite plus provider-side orchestration tokens when reported. */
|
|
77
77
|
totalTokens: number;
|
|
78
|
+
/** Provider-reported occupied context tokens when the value is authoritative but not a billable input/output bucket. */
|
|
79
|
+
contextTokens?: number;
|
|
78
80
|
/** Provider-side orchestration tokens, billed but not part of the conversation prompt/cache buckets. */
|
|
79
81
|
orchestration?: {
|
|
80
82
|
/** Non-cached orchestration input tokens. */
|
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.2.
|
|
4
|
+
"version": "17.2.3",
|
|
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,12 +35,12 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@bufbuild/protobuf": "^2.12.1",
|
|
38
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
38
|
+
"@oh-my-pi/pi-utils": "17.2.3",
|
|
39
39
|
"arktype": "2.2.3",
|
|
40
40
|
"zod": "^4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@oh-my-pi/pi-ai": "17.2.
|
|
43
|
+
"@oh-my-pi/pi-ai": "17.2.3",
|
|
44
44
|
"@types/bun": "^1.3.14"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
package/src/compat/openai.ts
CHANGED
|
@@ -322,10 +322,14 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
322
322
|
const isGrok = modelMatchesHost(hostModel, "xai");
|
|
323
323
|
const isMistral = modelMatchesHost(hostModel, "mistral");
|
|
324
324
|
const isOpenCodeHost = modelMatchesHost(hostModel, "opencode");
|
|
325
|
+
// Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)
|
|
326
|
+
// implements a subset of chat-completions and 400s on `store` ("Unknown name \"store\"").
|
|
327
|
+
const isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, "googleAistudio");
|
|
325
328
|
const isNonStandard =
|
|
326
329
|
isCerebras ||
|
|
327
330
|
isGrok ||
|
|
328
331
|
isMistral ||
|
|
332
|
+
isGoogleAistudioOpenAI ||
|
|
329
333
|
hostMatchesUrl(baseUrl, "chutes") ||
|
|
330
334
|
hostMatchesUrl(baseUrl, "deepseekFamily") ||
|
|
331
335
|
hostMatchesUrl(baseUrl, "fireworks") ||
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as http2 from "node:http2";
|
|
2
2
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
3
|
import { type } from "arktype";
|
|
4
|
+
import { isKimiK3ModelId } from "../identity";
|
|
5
|
+
import { bareModelId, parseGlmModel, semverGte } from "../identity/classify";
|
|
4
6
|
import { getBundledModels } from "../models";
|
|
5
7
|
import { toModelSpec } from "../provider-models/bundled-references";
|
|
6
8
|
import type { Model, ModelSpec } from "../types";
|
|
@@ -13,6 +15,19 @@ const CURSOR_GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
|
|
|
13
15
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
14
16
|
const DEFAULT_MAX_TOKENS = 64_000;
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* `GetUsableModels` carries no context-window field, so the 1M ceiling is
|
|
20
|
+
* recovered from the signals Cursor does send:
|
|
21
|
+
* - display-name labels ("Opus 5 1M", "GPT-5.5 1M High") across families,
|
|
22
|
+
* - natively 1M families Cursor serves unlabeled (Kimi K3, GLM 5.2+),
|
|
23
|
+
* - the max-mode flag on Claude/Gemini ids, whose max-mode ceiling is 1M.
|
|
24
|
+
*/
|
|
25
|
+
const CURSOR_1M_CONTEXT_WINDOW = 1_000_000;
|
|
26
|
+
const CURSOR_1M_NAME_PATTERN = /\b1m\b/i;
|
|
27
|
+
const CURSOR_MAX_MODE_1M_ID_PATTERN = /claude|gemini/;
|
|
28
|
+
/** Kimi's official bare K3 id (`k3`, `kimi/k3`); `k3-256k` is the 256k SKU and stays out. */
|
|
29
|
+
const CURSOR_KIMI_K3_BARE_ID_PATTERN = /(^|\/)k3$/i;
|
|
30
|
+
|
|
16
31
|
/**
|
|
17
32
|
* Model-id families whose native catalogs (anthropic, openai/openai-codex,
|
|
18
33
|
* google) are multimodal. Cursor-only or text-only families (`composer-*`,
|
|
@@ -282,7 +297,7 @@ function normalizeCursorModel(
|
|
|
282
297
|
|
|
283
298
|
const name = pickModelDisplayName(details, id);
|
|
284
299
|
const reference = references.get(id);
|
|
285
|
-
const reasoning = Boolean(details.thinkingDetails) || reference?.reasoning === true;
|
|
300
|
+
const reasoning = isKimiK3ModelId(id) || Boolean(details.thinkingDetails) || reference?.reasoning === true;
|
|
286
301
|
|
|
287
302
|
if (reference) {
|
|
288
303
|
return {
|
|
@@ -291,6 +306,7 @@ function normalizeCursorModel(
|
|
|
291
306
|
name,
|
|
292
307
|
baseUrl: baseUrlOverride ?? reference.baseUrl,
|
|
293
308
|
reasoning,
|
|
309
|
+
contextWindow: resolveCursorContextWindow(details, id, reference.contextWindow),
|
|
294
310
|
cursorMaxMode: details.maxMode,
|
|
295
311
|
};
|
|
296
312
|
}
|
|
@@ -303,12 +319,53 @@ function normalizeCursorModel(
|
|
|
303
319
|
reasoning,
|
|
304
320
|
input: inferInputFromCursorId(id),
|
|
305
321
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
306
|
-
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
322
|
+
contextWindow: resolveCursorContextWindow(details, id, DEFAULT_CONTEXT_WINDOW),
|
|
307
323
|
maxTokens: DEFAULT_MAX_TOKENS,
|
|
308
324
|
cursorMaxMode: details.maxMode,
|
|
309
325
|
};
|
|
310
326
|
}
|
|
311
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Context window for a discovered Cursor model: the 1M ceiling when any 1M
|
|
330
|
+
* signal fires (never below a larger bundled reference), else the fallback.
|
|
331
|
+
*/
|
|
332
|
+
function resolveCursorContextWindow(
|
|
333
|
+
model: CursorModelDetailsValue,
|
|
334
|
+
id: string,
|
|
335
|
+
fallback: number | null,
|
|
336
|
+
): number | null {
|
|
337
|
+
const labeled1M =
|
|
338
|
+
CURSOR_1M_NAME_PATTERN.test(id) ||
|
|
339
|
+
[model.displayName, model.displayNameShort, model.displayModelId, ...model.aliases].some(
|
|
340
|
+
candidate => typeof candidate === "string" && CURSOR_1M_NAME_PATTERN.test(candidate),
|
|
341
|
+
);
|
|
342
|
+
if (labeled1M || isCursorNative1MModelId(id) || (model.maxMode && CURSOR_MAX_MODE_1M_ID_PATTERN.test(id))) {
|
|
343
|
+
return Math.max(fallback ?? 0, CURSOR_1M_CONTEXT_WINDOW);
|
|
344
|
+
}
|
|
345
|
+
return fallback;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Natively 1M-context families Cursor serves without a "1M" label: Kimi K3 and
|
|
350
|
+
* GLM 5.2+ coding SKUs. The shared family parsers cover namespace forms
|
|
351
|
+
* (`moonshotai/kimi-k3`, `z-ai/glm-5.2`) and future GLM versions (`glm-5.10`,
|
|
352
|
+
* `glm-6`); vision and sub-1M variants stay out via the same gates as
|
|
353
|
+
* `isGlm52ReasoningEffortModelId`.
|
|
354
|
+
*/
|
|
355
|
+
function isCursorNative1MModelId(id: string): boolean {
|
|
356
|
+
if (isKimiK3ModelId(id) || CURSOR_KIMI_K3_BARE_ID_PATTERN.test(id)) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
const glm = parseGlmModel(bareModelId(id));
|
|
360
|
+
if (!glm || glm.vision) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return semverGte(glm.version, "5.2");
|
|
367
|
+
}
|
|
368
|
+
|
|
312
369
|
function pickModelDisplayName(model: CursorModelDetailsValue, fallbackId: string): string {
|
|
313
370
|
const candidates = [model.displayName, model.displayNameShort, model.displayModelId, ...model.aliases, fallbackId];
|
|
314
371
|
for (const candidate of candidates) {
|
package/src/hosts.ts
CHANGED
|
@@ -62,6 +62,8 @@ export const KNOWN_HOSTS = {
|
|
|
62
62
|
/** NVIDIA NIM (`integrate.api.nvidia.com`). Qwen NIM endpoints take `chat_template_kwargs.enable_thinking`, never top-level `enable_thinking`. */
|
|
63
63
|
nvidia: { providers: ["nvidia"], urlMarkers: ["integrate.api.nvidia.com"] },
|
|
64
64
|
moonshotNative: { providers: ["moonshot", "kimi-code"], urlMarkers: ["api.moonshot.ai", "api.kimi.com"] },
|
|
65
|
+
/** Google AI Studio's OpenAI-compatible shim (`/v1beta/openai`) — a subset of chat-completions; rejects `store` with a 400. Native Gemini uses `google-generative-ai` api instead. */
|
|
66
|
+
googleAistudio: { providers: [], urlMarkers: ["generativelanguage.googleapis.com"] },
|
|
65
67
|
opencode: { providers: ["opencode-go", "opencode-zen"], urlMarkers: ["opencode.ai"] },
|
|
66
68
|
/** ZenMux's Anthropic-compatible proxy (`zenmux.ai/api/anthropic`) forwards to signature-enforcing Anthropic. */
|
|
67
69
|
zenmux: { providers: ["zenmux"], urlMarkers: ["zenmux.ai"] },
|
package/src/model-manager.ts
CHANGED
|
@@ -14,10 +14,10 @@ const NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
|
|
|
14
14
|
export type ModelRefreshStrategy = "online" | "offline" | "online-if-uncached";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Hook for loading and mapping
|
|
17
|
+
* Hook for loading and mapping stencil.so fallback data into canonical model objects.
|
|
18
18
|
*/
|
|
19
19
|
export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
|
|
20
|
-
/** Fetches raw fallback payload (for example from
|
|
20
|
+
/** Fetches raw fallback payload (for example from stencil.so). */
|
|
21
21
|
fetch(): Promise<TPayload>;
|
|
22
22
|
/** Maps payload into provider models. */
|
|
23
23
|
map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
|
|
@@ -39,7 +39,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
39
39
|
cacheTtlMs?: number;
|
|
40
40
|
/** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
|
|
41
41
|
dynamicModelsAuthoritative?: boolean;
|
|
42
|
-
/** Cached model ids
|
|
42
|
+
/** Cached model ids whose presence forces refresh when the static or migration-policy fingerprint changes. */
|
|
43
43
|
dropCachedModelIdsOnStaticMismatch?: readonly string[];
|
|
44
44
|
/**
|
|
45
45
|
* Trusted, provider-wide request headers (compile-time constants, never
|
|
@@ -51,7 +51,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
51
51
|
restorableHeaderFallback?: Record<string, string>;
|
|
52
52
|
/** Optional dynamic endpoint fetcher. */
|
|
53
53
|
fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
|
|
54
|
-
/** Optional
|
|
54
|
+
/** Optional stencil.so fallback hook. */
|
|
55
55
|
modelsDev?: ModelsDevFallback<TApi, TModelsDevPayload>;
|
|
56
56
|
/** Clock override for deterministic tests. */
|
|
57
57
|
now?: () => number;
|
|
@@ -171,7 +171,7 @@ function restoreCachedModelHeaders<TApi extends Api>(
|
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
173
|
* Resolves provider models with source precedence:
|
|
174
|
-
* static ->
|
|
174
|
+
* static -> stencil.so -> cache -> dynamic.
|
|
175
175
|
*
|
|
176
176
|
* Later sources override earlier ones by model id.
|
|
177
177
|
*/
|
|
@@ -199,10 +199,24 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
199
199
|
const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
|
|
200
200
|
const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
|
|
201
201
|
const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
|
|
202
|
-
const
|
|
202
|
+
const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
|
|
203
|
+
const staticCatalogFingerprint = fingerprintStatic(staticModels, dynamicModelsAuthoritative);
|
|
204
|
+
// Endpoint-migration policy is cache identity: adding an id must invalidate
|
|
205
|
+
// matching-static-catalog caches written by the prior resolver.
|
|
206
|
+
const staticFingerprint =
|
|
207
|
+
cacheDropIds && cacheDropIds.length > 0
|
|
208
|
+
? `${staticCatalogFingerprint}:drop:${Bun.hash(cacheDropIds.join("\0")).toString(36)}`
|
|
209
|
+
: staticCatalogFingerprint;
|
|
203
210
|
const cacheFingerprintMatches = cache?.staticFingerprint === staticFingerprint && staticFingerprint.length > 0;
|
|
211
|
+
const cacheNeedsModelMigration =
|
|
212
|
+
!cacheFingerprintMatches &&
|
|
213
|
+
cacheDropIds !== undefined &&
|
|
214
|
+
usableCachedModels.some(model => cacheDropIds.includes(model.id));
|
|
204
215
|
const hasUsableFreshCache =
|
|
205
|
-
(cache?.fresh ?? false) &&
|
|
216
|
+
(cache?.fresh ?? false) &&
|
|
217
|
+
!cacheHasUnresolvedHeaders &&
|
|
218
|
+
!cacheNeedsModelMigration &&
|
|
219
|
+
(!dynamicModelsAuthoritative || cacheFingerprintMatches);
|
|
206
220
|
const dynamicFetcher = options.fetchDynamicModels;
|
|
207
221
|
const hasDynamicFetcher = typeof dynamicFetcher === "function";
|
|
208
222
|
const hasAuthoritativeCache = ((cache?.authoritative ?? false) && hasUsableFreshCache) || !hasDynamicFetcher;
|
|
@@ -484,13 +498,25 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
484
498
|
const supportsImage = dynamicInputAuthoritative
|
|
485
499
|
? dynamicModel.input.includes("image")
|
|
486
500
|
: existingModel.input.includes("image") || dynamicModel.input.includes("image");
|
|
501
|
+
// Synthetic's discovery is authoritative (`dynamicModelsAuthoritative`) and
|
|
502
|
+
// its per-model `reasoning_parameters.efforts` vocabulary is the route's
|
|
503
|
+
// whole truth: when the wire advertises only the `none` off-state the
|
|
504
|
+
// mapper emits `reasoning: false`, and OR-ing the bundled reference's
|
|
505
|
+
// stale `reasoning: true` back would re-arm an effort dial the route
|
|
506
|
+
// doesn't expose. Other providers keep the OR so a bundled reasoning flag
|
|
507
|
+
// survives a discovery row that simply omits the capability.
|
|
508
|
+
const dynamicReasoningAuthoritative =
|
|
509
|
+
existingModel.provider === "synthetic" && dynamicModel.provider === "synthetic";
|
|
510
|
+
const reasoning = dynamicReasoningAuthoritative
|
|
511
|
+
? dynamicModel.reasoning
|
|
512
|
+
: existingModel.reasoning || dynamicModel.reasoning;
|
|
487
513
|
// Re-build from spec stage: sparse compat comes from `compatConfig` (the
|
|
488
514
|
// verbatim override vocabulary), never the resolved `compat` record.
|
|
489
515
|
return buildModel({
|
|
490
516
|
...existingModel,
|
|
491
517
|
...dynamicModel,
|
|
492
518
|
name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
|
|
493
|
-
reasoning
|
|
519
|
+
reasoning,
|
|
494
520
|
input: supportsImage ? ["text", "image"] : ["text"],
|
|
495
521
|
cost: {
|
|
496
522
|
input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input),
|