@oh-my-pi/pi-catalog 16.0.7 → 16.0.9
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 +17 -0
- package/dist/types/identity/classify.d.ts +0 -1
- package/dist/types/identity/family.d.ts +22 -22
- package/dist/types/provider-models/openai-compat.d.ts +12 -0
- package/dist/types/types.d.ts +4 -1
- package/package.json +3 -3
- package/src/compat/anthropic.ts +10 -1
- package/src/compat/openai.ts +7 -0
- package/src/hosts.ts +15 -3
- package/src/identity/classify.ts +7 -1
- package/src/identity/family.ts +59 -44
- package/src/provider-models/openai-compat.ts +105 -3
- package/src/types.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.9] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed GitHub Copilot's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`). It forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: ""` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused a `400 Invalid signature` that corrupted the session and re-tripped on every full history re-send (e.g. after toggling MCP servers). Copilot now degrades such blocks to text like the official API. ([#2851](https://github.com/can1357/oh-my-pi/issues/2851))
|
|
10
|
+
- Added a `supportsImageDetailOriginal` compat flag that resolves to `false` for GitHub Copilot, whose Responses endpoint rejects the `detail: "original"` image hint with a 400, and `true` for every other host. ([#2822](https://github.com/can1357/oh-my-pi/issues/2822))
|
|
11
|
+
|
|
12
|
+
## [16.0.8] - 2026-06-18
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Refactored model family ID predicates and capability checkers to use a shared, uniform process-lifetime `memo` utility to eliminate caching boilerplate.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed LM Studio dynamic discovery to use native `/api/v0/models` metadata so VLM models advertise image input. ([#2945](https://github.com/can1357/oh-my-pi/issues/2945))
|
|
21
|
+
|
|
5
22
|
## [16.0.7] - 2026-06-18
|
|
6
23
|
|
|
7
24
|
### Fixed
|
|
@@ -41,7 +41,6 @@ export interface UnknownModel {
|
|
|
41
41
|
id: string;
|
|
42
42
|
}
|
|
43
43
|
export type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
|
|
44
|
-
/** Strip a provider namespace prefix (`openai/gpt-5.4` → `gpt-5.4`). */
|
|
45
44
|
export declare function bareModelId(modelId: string): string;
|
|
46
45
|
export declare function parseKnownModel(modelId: string): ParsedModel;
|
|
47
46
|
export declare const parseGeminiModel: (modelId: string) => GeminiModel | null;
|
|
@@ -7,27 +7,27 @@
|
|
|
7
7
|
* here.
|
|
8
8
|
*/
|
|
9
9
|
/** Kimi family ids in any namespace form (`moonshotai/kimi-*`, `kimi-k2.6`, `vendor/kimi.x`). */
|
|
10
|
-
export declare
|
|
10
|
+
export declare const isKimiModelId: (modelId: string) => boolean;
|
|
11
11
|
/** Kimi K2.6 specifically, including router ids that spell the version `k2p6`. */
|
|
12
|
-
export declare
|
|
12
|
+
export declare const isKimiK26ModelId: (modelId: string) => boolean;
|
|
13
13
|
/** Claude ids in any namespace form (`claude-*`, `vendor/claude.x`). */
|
|
14
|
-
export declare
|
|
14
|
+
export declare const isClaudeModelId: (modelId: string) => boolean;
|
|
15
15
|
/** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
|
|
16
|
-
export declare
|
|
16
|
+
export declare const isAnthropicNamespacedModelId: (modelId: string) => boolean;
|
|
17
17
|
/** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
|
|
18
|
-
export declare
|
|
18
|
+
export declare const isQwenModelId: (modelId: string) => boolean;
|
|
19
19
|
/** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
|
|
20
|
-
export declare
|
|
20
|
+
export declare const isGemmaModelId: (modelId: string) => boolean;
|
|
21
21
|
/** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
|
|
22
|
-
export declare
|
|
22
|
+
export declare const isDeepseekModelIdOrName: (modelId: string) => boolean;
|
|
23
23
|
/** Xiaomi MiMo family by id or display name. */
|
|
24
|
-
export declare
|
|
24
|
+
export declare const isMimoModelIdOrName: (modelId: string) => boolean;
|
|
25
25
|
/**
|
|
26
26
|
* Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
|
|
27
27
|
* (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
|
|
28
28
|
* param, so callers must omit reasoning effort for them.
|
|
29
29
|
*/
|
|
30
|
-
export declare
|
|
30
|
+
export declare const isGrokReasoningEffortCapable: (modelId: string) => boolean;
|
|
31
31
|
/**
|
|
32
32
|
* MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
|
|
33
33
|
* `-lightning`/`-her`/`-turbo` variants, dotless aliases like `minimax-m21`,
|
|
@@ -37,18 +37,18 @@ export declare function isGrokReasoningEffortCapable(modelId: string): boolean;
|
|
|
37
37
|
* `minimal` to `none` (Fireworks) or expects the full 5-tier scale must
|
|
38
38
|
* clamp instead. Excludes M1, M3, MiniMax-Text-01, music, hailuo, voice ids.
|
|
39
39
|
*/
|
|
40
|
-
export declare
|
|
40
|
+
export declare const isMinimaxM2FamilyModelId: (modelId: string) => boolean;
|
|
41
41
|
/** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
|
|
42
|
-
export declare
|
|
42
|
+
export declare const isMinimaxM3FamilyModelId: (modelId: string) => boolean;
|
|
43
43
|
/**
|
|
44
44
|
* OpenAI gpt-oss family (`gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss:120b`,
|
|
45
45
|
* `vendor/gpt-oss-…`). The Harmony reasoning format only accepts
|
|
46
46
|
* `low|medium|high` for `reasoning_effort` and rejects `minimal`, `xhigh`,
|
|
47
47
|
* and `none`.
|
|
48
48
|
*/
|
|
49
|
-
export declare
|
|
49
|
+
export declare const isOpenAIGptOssModelId: (modelId: string) => boolean;
|
|
50
50
|
/** OpenAI model ids (gpt-*, o1-*, o3-*, o4-*, or prefixed with openai/). */
|
|
51
|
-
export declare
|
|
51
|
+
export declare const isOpenAIModelId: (modelId: string) => boolean;
|
|
52
52
|
/**
|
|
53
53
|
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
|
54
54
|
* `-turbo` lines. Excludes the vision (`…v`) shape, the non-reasoning
|
|
@@ -56,11 +56,11 @@ export declare function isOpenAIModelId(modelId: string): boolean;
|
|
|
56
56
|
* keeps newly-bumped integers (`glm-5.3`, `glm-6`, …) covered without a per-id
|
|
57
57
|
* allowlist.
|
|
58
58
|
*/
|
|
59
|
-
export declare
|
|
59
|
+
export declare const isReasoningGlmModelId: (modelId: string) => boolean;
|
|
60
60
|
/** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
|
|
61
|
-
export declare
|
|
61
|
+
export declare const isGlm52ReasoningEffortModelId: (modelId: string) => boolean;
|
|
62
62
|
/** GLM vision SKUs — the `v` that attaches to the version (`glm-4v`, `glm-4.5v`). */
|
|
63
|
-
export declare
|
|
63
|
+
export declare const isGlmVisionModelId: (modelId: string) => boolean;
|
|
64
64
|
/**
|
|
65
65
|
* Coarse vendor-lineage token for "are two models the same family?" checks
|
|
66
66
|
* (e.g. picking a cross-family reviewer). All Claude point releases share a token,
|
|
@@ -72,7 +72,7 @@ export declare function isGlmVisionModelId(modelId: string): boolean;
|
|
|
72
72
|
* Vendor-only by design: a model's kind/variant (opus vs sonnet, codex vs base) is
|
|
73
73
|
* collapsed onto the single vendor token; use {@link parseKnownModel} for finer breakdowns.
|
|
74
74
|
*/
|
|
75
|
-
export declare
|
|
75
|
+
export declare const modelFamilyToken: (modelId: string) => string;
|
|
76
76
|
/**
|
|
77
77
|
* Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
|
|
78
78
|
* the Claude Fable/Mythos 5 generation. Older adaptive-thinking models
|
|
@@ -80,13 +80,13 @@ export declare function modelFamilyToken(modelId: string): string;
|
|
|
80
80
|
* dashed version forms both match while bare dated ids
|
|
81
81
|
* (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
|
|
82
82
|
*/
|
|
83
|
-
export declare
|
|
83
|
+
export declare const supportsAdaptiveThinkingDisplay: (modelId: string) => boolean;
|
|
84
84
|
/**
|
|
85
85
|
* Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
|
|
86
86
|
* - Sampling parameters (temperature/top_p/top_k) return 400 error
|
|
87
87
|
* - Thinking content is omitted by default (needs display: "summarized")
|
|
88
88
|
*/
|
|
89
|
-
export declare
|
|
89
|
+
export declare const hasOpus47ApiRestrictions: (modelId: string) => boolean;
|
|
90
90
|
/**
|
|
91
91
|
* Mid-conversation `role: "system"` messages (system instructions appended at
|
|
92
92
|
* non-first positions in the `messages` array) are supported starting with
|
|
@@ -94,8 +94,8 @@ export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
|
|
|
94
94
|
* models reject the role.
|
|
95
95
|
* @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
|
|
96
96
|
*/
|
|
97
|
-
export declare
|
|
98
|
-
export declare
|
|
97
|
+
export declare const supportsMidConversationSystemMessages: (modelId: string) => boolean;
|
|
98
|
+
export declare const isAnthropicFableOrMythosModel: (modelId: string) => boolean;
|
|
99
99
|
/** Thinking-variant token location inside a model id. */
|
|
100
100
|
export interface ThinkingVariantToken {
|
|
101
101
|
index: number;
|
|
@@ -115,4 +115,4 @@ export declare function findThinkingVariantToken(modelId: string): ThinkingVaria
|
|
|
115
115
|
* token exists or nothing would remain. Callers MUST verify the result names
|
|
116
116
|
* a live model.
|
|
117
117
|
*/
|
|
118
|
-
export declare
|
|
118
|
+
export declare const stripThinkingVariantToken: (modelId: string) => string | undefined;
|
|
@@ -265,6 +265,18 @@ export interface KimiCodeModelManagerConfig {
|
|
|
265
265
|
fetch?: FetchImpl;
|
|
266
266
|
}
|
|
267
267
|
export declare function kimiCodeModelManagerOptions(config?: KimiCodeModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
268
|
+
/** Native LM Studio metadata keyed by model id from `/api/v0/models`. */
|
|
269
|
+
export interface LmStudioNativeModelMetadata {
|
|
270
|
+
input: ("text" | "image")[];
|
|
271
|
+
contextWindow?: number;
|
|
272
|
+
}
|
|
273
|
+
/** Options for LM Studio's optional native metadata probe. */
|
|
274
|
+
export interface LmStudioNativeModelMetadataOptions {
|
|
275
|
+
headers?: Record<string, string>;
|
|
276
|
+
signal?: AbortSignal;
|
|
277
|
+
}
|
|
278
|
+
/** Fetches LM Studio native model metadata used to mark VLM models as image-capable. */
|
|
279
|
+
export declare function fetchLmStudioNativeModelMetadata(baseUrl: string, fetchImpl?: FetchImpl, options?: LmStudioNativeModelMetadataOptions): Promise<Map<string, LmStudioNativeModelMetadata> | null>;
|
|
268
280
|
export interface LmStudioModelManagerConfig {
|
|
269
281
|
apiKey?: string;
|
|
270
282
|
baseUrl?: string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -247,6 +247,8 @@ export interface OpenAICompat {
|
|
|
247
247
|
alwaysSendMaxTokens?: boolean;
|
|
248
248
|
/** Whether Responses-API tool-call/result history must be strictly paired. Default: auto-detected (Azure OpenAI, GitHub Copilot). */
|
|
249
249
|
strictResponsesPairing?: boolean;
|
|
250
|
+
/** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
|
|
251
|
+
supportsImageDetailOriginal?: boolean;
|
|
250
252
|
/**
|
|
251
253
|
* Append a trailing `# Juice: 0 !important` developer item when the caller
|
|
252
254
|
* did not request reasoning, suppressing default reasoning on models that
|
|
@@ -416,7 +418,7 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
416
418
|
* `buildModel`; request handlers read fields and never detect, resolve, or
|
|
417
419
|
* allocate.
|
|
418
420
|
*/
|
|
419
|
-
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "requiresJuiceZeroHack" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
|
|
421
|
+
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "requiresJuiceZeroHack" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
|
|
420
422
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
421
423
|
extraBody?: OpenAICompat["extraBody"];
|
|
422
424
|
cacheControlFormat?: OpenAICompat["cacheControlFormat"];
|
|
@@ -434,6 +436,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<Op
|
|
|
434
436
|
export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompat {
|
|
435
437
|
supportsLongPromptCacheRetention: boolean;
|
|
436
438
|
strictResponsesPairing: boolean;
|
|
439
|
+
supportsImageDetailOriginal: boolean;
|
|
437
440
|
requiresJuiceZeroHack: boolean;
|
|
438
441
|
supportsObfuscationOptOut: boolean;
|
|
439
442
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-catalog",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.9",
|
|
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",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@bufbuild/protobuf": "^2.12.0",
|
|
37
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
37
|
+
"@oh-my-pi/pi-utils": "16.0.9",
|
|
38
38
|
"arktype": "^2.2.0",
|
|
39
39
|
"zod": "^4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.0.9",
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
package/src/compat/anthropic.ts
CHANGED
|
@@ -66,7 +66,16 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
|
|
|
66
66
|
// loses the reasoning chain and can destabilize the next tool-call
|
|
67
67
|
// arguments (#2005). Known non-signing hosts (Z.AI, DeepSeek) are also
|
|
68
68
|
// preserved for compatibility.
|
|
69
|
-
|
|
69
|
+
//
|
|
70
|
+
// GitHub Copilot's `anthropic-messages` proxy is excluded: it forwards to
|
|
71
|
+
// signature-enforcing Anthropic and returns full thinking signatures, so it
|
|
72
|
+
// is a SIGNING endpoint. Replaying a stripped/unsigned thinking block as
|
|
73
|
+
// `signature: ""` there 400s the whole request ("Invalid signature") — most
|
|
74
|
+
// visibly when a checkpoint/branch-return turn's end_turn-bound signature is
|
|
75
|
+
// stripped on replay (issue #2851). Treating it like official Anthropic
|
|
76
|
+
// degrades such blocks to text instead, which the API accepts.
|
|
77
|
+
replayUnsignedThinking:
|
|
78
|
+
!isCopilot && (isZai || modelMatchesHost(spec, "deepseekFamily") || (spec.reasoning && !official)),
|
|
70
79
|
escapeBuiltinToolNames: modelMatchesHost(spec, "umans"),
|
|
71
80
|
};
|
|
72
81
|
applyCompatOverrides(compat, spec.compat);
|
package/src/compat/openai.ts
CHANGED
|
@@ -459,6 +459,12 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
459
459
|
// Azure OpenAI and GitHub Copilot Responses paths require tool results
|
|
460
460
|
// to strictly match prior tool calls when building Responses inputs.
|
|
461
461
|
strictResponsesPairing: isAzure || spec.provider === "github-copilot",
|
|
462
|
+
// GitHub Copilot's Responses endpoint rejects the `detail: "original"`
|
|
463
|
+
// image hint with a 400; every other host preserves native-resolution
|
|
464
|
+
// frames (snapcompact relies on `original`). Detect Copilot by provider id
|
|
465
|
+
// or base-URL host (mirroring the Anthropic compat builder) so a model
|
|
466
|
+
// pointed at the Copilot host under a different provider id still clamps.
|
|
467
|
+
supportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
|
|
462
468
|
requiresJuiceZeroHack: spec.name.toLowerCase().startsWith("gpt-5"),
|
|
463
469
|
reasoningEffortMap: {},
|
|
464
470
|
supportsReasoningParams: true,
|
|
@@ -514,6 +520,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
|
|
|
514
520
|
return {
|
|
515
521
|
supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,
|
|
516
522
|
strictResponsesPairing: compat.strictResponsesPairing,
|
|
523
|
+
supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
|
|
517
524
|
requiresJuiceZeroHack: compat.requiresJuiceZeroHack,
|
|
518
525
|
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
|
|
519
526
|
} satisfies ResponsesOnlyCompat;
|
package/src/hosts.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface HostClassSpec {
|
|
|
16
16
|
readonly providers?: readonly string[];
|
|
17
17
|
/** Provider-id prefixes that imply this host class (e.g. `xiaomi-token-plan-`). */
|
|
18
18
|
readonly providerPrefixes?: readonly string[];
|
|
19
|
-
/**
|
|
19
|
+
/** Lowercase ASCII substrings matched case-insensitively against the base URL. */
|
|
20
20
|
readonly urlMarkers: readonly string[];
|
|
21
21
|
// Strict hostname matching is intentionally not modeled here: the one
|
|
22
22
|
// auth-sensitive consumer (Anthropic official-endpoint) parses the URL
|
|
@@ -68,9 +68,8 @@ export type KnownHost = keyof typeof KNOWN_HOSTS;
|
|
|
68
68
|
export function hostMatchesUrl(baseUrl: string | undefined, host: KnownHost): boolean {
|
|
69
69
|
if (!baseUrl) return false;
|
|
70
70
|
const spec: HostClassSpec = KNOWN_HOSTS[host];
|
|
71
|
-
const normalized = baseUrl.toLowerCase();
|
|
72
71
|
for (const marker of spec.urlMarkers) {
|
|
73
|
-
if (
|
|
72
|
+
if (includesAsciiCaseInsensitive(baseUrl, marker)) return true;
|
|
74
73
|
}
|
|
75
74
|
return false;
|
|
76
75
|
}
|
|
@@ -91,6 +90,19 @@ export function modelMatchesHost(model: { provider: string; baseUrl: string }, h
|
|
|
91
90
|
return hostMatchesUrl(model.baseUrl, host);
|
|
92
91
|
}
|
|
93
92
|
|
|
93
|
+
function includesAsciiCaseInsensitive(value: string, lowerNeedle: string): boolean {
|
|
94
|
+
const needleLength = lowerNeedle.length;
|
|
95
|
+
const end = value.length - needleLength;
|
|
96
|
+
for (let start = 0; start <= end; start++) {
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (; offset < needleLength; offset++) {
|
|
99
|
+
if ((value.charCodeAt(start + offset) | 0x20) !== lowerNeedle.charCodeAt(offset)) break;
|
|
100
|
+
}
|
|
101
|
+
if (offset === needleLength) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
// --- Endpoint-shape predicates (URL path/verb shapes, not vendor hosts) ---
|
|
95
107
|
|
|
96
108
|
/** Vertex AI express-mode OpenAI-compatible endpoint (`…/endpoints/openapi`). */
|
package/src/identity/classify.ts
CHANGED
|
@@ -51,9 +51,15 @@ export interface UnknownModel {
|
|
|
51
51
|
export type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
|
|
52
52
|
|
|
53
53
|
/** Strip a provider namespace prefix (`openai/gpt-5.4` → `gpt-5.4`). */
|
|
54
|
+
// Cache keyed by model id (a bounded set of bundled/aggregator ids), so no eviction is needed.
|
|
55
|
+
const bareModelIdCache = new Map<string, string>();
|
|
54
56
|
export function bareModelId(modelId: string): string {
|
|
57
|
+
const cached = bareModelIdCache.get(modelId);
|
|
58
|
+
if (cached !== undefined) return cached;
|
|
55
59
|
const p = modelId.lastIndexOf("/");
|
|
56
|
-
|
|
60
|
+
const result = p !== -1 ? modelId.slice(p + 1) : modelId;
|
|
61
|
+
bareModelIdCache.set(modelId, result);
|
|
62
|
+
return result;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export function parseKnownModel(modelId: string): ParsedModel {
|
package/src/identity/family.ts
CHANGED
|
@@ -16,45 +16,58 @@ import {
|
|
|
16
16
|
semverGte,
|
|
17
17
|
} from "./classify";
|
|
18
18
|
|
|
19
|
+
/** Bounded process-lifetime cache memo helper. */
|
|
20
|
+
function memo<T>(fn: (modelId: string) => T): (modelId: string) => T {
|
|
21
|
+
const cache = new Map<string, T>();
|
|
22
|
+
return (modelId: string) => {
|
|
23
|
+
if (cache.has(modelId)) {
|
|
24
|
+
return cache.get(modelId) as T;
|
|
25
|
+
}
|
|
26
|
+
const result = fn(modelId);
|
|
27
|
+
cache.set(modelId, result);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
/** Kimi family ids in any namespace form (`moonshotai/kimi-*`, `kimi-k2.6`, `vendor/kimi.x`). */
|
|
20
|
-
export
|
|
33
|
+
export const isKimiModelId = memo((modelId: string): boolean => {
|
|
21
34
|
return modelId.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(modelId);
|
|
22
|
-
}
|
|
35
|
+
});
|
|
23
36
|
|
|
24
37
|
/** Kimi K2.6 specifically, including router ids that spell the version `k2p6`. */
|
|
25
|
-
export
|
|
38
|
+
export const isKimiK26ModelId = memo((modelId: string): boolean => {
|
|
26
39
|
return /(^|\/)kimi-k2(?:\.6|p6)(?:[-:]|$)/i.test(modelId);
|
|
27
|
-
}
|
|
40
|
+
});
|
|
28
41
|
|
|
29
42
|
/** Claude ids in any namespace form (`claude-*`, `vendor/claude.x`). */
|
|
30
|
-
export
|
|
43
|
+
export const isClaudeModelId = memo((modelId: string): boolean => {
|
|
31
44
|
return /(^|\/)claude[-.]/i.test(modelId);
|
|
32
|
-
}
|
|
45
|
+
});
|
|
33
46
|
|
|
34
47
|
/** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
|
|
35
|
-
export
|
|
48
|
+
export const isAnthropicNamespacedModelId = memo((modelId: string): boolean => {
|
|
36
49
|
return /(^|\/)anthropic\//i.test(modelId);
|
|
37
|
-
}
|
|
50
|
+
});
|
|
38
51
|
|
|
39
52
|
/** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
|
|
40
|
-
export
|
|
53
|
+
export const isQwenModelId = memo((modelId: string): boolean => {
|
|
41
54
|
return modelId.toLowerCase().includes("qwen");
|
|
42
|
-
}
|
|
55
|
+
});
|
|
43
56
|
|
|
44
57
|
/** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
|
|
45
|
-
export
|
|
58
|
+
export const isGemmaModelId = memo((modelId: string): boolean => {
|
|
46
59
|
return /(^|\/)gemma[-.]?\d/i.test(modelId);
|
|
47
|
-
}
|
|
60
|
+
});
|
|
48
61
|
|
|
49
62
|
/** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
|
|
50
|
-
export
|
|
63
|
+
export const isDeepseekModelIdOrName = memo((value: string): boolean => {
|
|
51
64
|
return value.toLowerCase().includes("deepseek");
|
|
52
|
-
}
|
|
65
|
+
});
|
|
53
66
|
|
|
54
67
|
/** Xiaomi MiMo family by id or display name. */
|
|
55
|
-
export
|
|
68
|
+
export const isMimoModelIdOrName = memo((value: string): boolean => {
|
|
56
69
|
return value.toLowerCase().includes("mimo");
|
|
57
|
-
}
|
|
70
|
+
});
|
|
58
71
|
|
|
59
72
|
const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3"] as const;
|
|
60
73
|
|
|
@@ -63,11 +76,11 @@ const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "g
|
|
|
63
76
|
* (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
|
|
64
77
|
* param, so callers must omit reasoning effort for them.
|
|
65
78
|
*/
|
|
66
|
-
export
|
|
79
|
+
export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
|
|
67
80
|
const bare = bareModelId(modelId).trim().toLowerCase();
|
|
68
81
|
if (!bare) return false;
|
|
69
82
|
return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
|
|
70
|
-
}
|
|
83
|
+
});
|
|
71
84
|
|
|
72
85
|
/**
|
|
73
86
|
* MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
|
|
@@ -78,20 +91,20 @@ export function isGrokReasoningEffortCapable(modelId: string): boolean {
|
|
|
78
91
|
* `minimal` to `none` (Fireworks) or expects the full 5-tier scale must
|
|
79
92
|
* clamp instead. Excludes M1, M3, MiniMax-Text-01, music, hailuo, voice ids.
|
|
80
93
|
*/
|
|
81
|
-
export
|
|
94
|
+
export const isMinimaxM2FamilyModelId = memo((modelId: string): boolean => {
|
|
82
95
|
const lower = modelId.toLowerCase();
|
|
83
96
|
if (!lower.includes("minimax")) return false;
|
|
84
97
|
// Boundary-delimited `m2` token followed by zero or more digits (dotless
|
|
85
98
|
// variants like `m21`/`m25`/`m27`) and an optional dotted minor version.
|
|
86
99
|
return /(?:^|[/.-])m2\d*(?:[.-]\d+)?(?:[-.:_]|$)/i.test(lower);
|
|
87
|
-
}
|
|
100
|
+
});
|
|
88
101
|
|
|
89
102
|
/** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
|
|
90
|
-
export
|
|
103
|
+
export const isMinimaxM3FamilyModelId = memo((modelId: string): boolean => {
|
|
91
104
|
const lower = modelId.toLowerCase();
|
|
92
105
|
if (!lower.includes("minimax")) return false;
|
|
93
106
|
return /(?:^|[/._-])(?:minimax[/._-])?m3(?:[-.:_]|$)/i.test(lower);
|
|
94
|
-
}
|
|
107
|
+
});
|
|
95
108
|
|
|
96
109
|
/**
|
|
97
110
|
* OpenAI gpt-oss family (`gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss:120b`,
|
|
@@ -99,14 +112,14 @@ export function isMinimaxM3FamilyModelId(modelId: string): boolean {
|
|
|
99
112
|
* `low|medium|high` for `reasoning_effort` and rejects `minimal`, `xhigh`,
|
|
100
113
|
* and `none`.
|
|
101
114
|
*/
|
|
102
|
-
export
|
|
115
|
+
export const isOpenAIGptOssModelId = memo((modelId: string): boolean => {
|
|
103
116
|
return /(^|\/)gpt-oss[-:]/i.test(modelId);
|
|
104
|
-
}
|
|
117
|
+
});
|
|
105
118
|
|
|
106
119
|
/** OpenAI model ids (gpt-*, o1-*, o3-*, o4-*, or prefixed with openai/). */
|
|
107
|
-
export
|
|
120
|
+
export const isOpenAIModelId = memo((modelId: string): boolean => {
|
|
108
121
|
return /(^|\/)(gpt|o1|o3|o4)[-.]/i.test(modelId) || modelId.toLowerCase().includes("openai/");
|
|
109
|
-
}
|
|
122
|
+
});
|
|
110
123
|
|
|
111
124
|
/**
|
|
112
125
|
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
|
@@ -115,7 +128,7 @@ export function isOpenAIModelId(modelId: string): boolean {
|
|
|
115
128
|
* keeps newly-bumped integers (`glm-5.3`, `glm-6`, …) covered without a per-id
|
|
116
129
|
* allowlist.
|
|
117
130
|
*/
|
|
118
|
-
export
|
|
131
|
+
export const isReasoningGlmModelId = memo((modelId: string): boolean => {
|
|
119
132
|
const glm = parseGlmModel(bareModelId(modelId));
|
|
120
133
|
if (!glm || glm.vision) {
|
|
121
134
|
return false;
|
|
@@ -124,9 +137,10 @@ export function isReasoningGlmModelId(modelId: string): boolean {
|
|
|
124
137
|
return false;
|
|
125
138
|
}
|
|
126
139
|
return semverGte(glm.version, "4.5");
|
|
127
|
-
}
|
|
140
|
+
});
|
|
141
|
+
|
|
128
142
|
/** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
|
|
129
|
-
export
|
|
143
|
+
export const isGlm52ReasoningEffortModelId = memo((modelId: string): boolean => {
|
|
130
144
|
const glm = parseGlmModel(bareModelId(modelId));
|
|
131
145
|
if (!glm || glm.vision) {
|
|
132
146
|
return false;
|
|
@@ -135,12 +149,13 @@ export function isGlm52ReasoningEffortModelId(modelId: string): boolean {
|
|
|
135
149
|
return false;
|
|
136
150
|
}
|
|
137
151
|
return semverGte(glm.version, "5.2");
|
|
138
|
-
}
|
|
152
|
+
});
|
|
139
153
|
|
|
140
154
|
/** GLM vision SKUs — the `v` that attaches to the version (`glm-4v`, `glm-4.5v`). */
|
|
141
|
-
export
|
|
155
|
+
export const isGlmVisionModelId = memo((modelId: string): boolean => {
|
|
142
156
|
return parseGlmModel(bareModelId(modelId))?.vision === true;
|
|
143
|
-
}
|
|
157
|
+
});
|
|
158
|
+
|
|
144
159
|
/**
|
|
145
160
|
* Coarse vendor-lineage token for "are two models the same family?" checks
|
|
146
161
|
* (e.g. picking a cross-family reviewer). All Claude point releases share a token,
|
|
@@ -152,7 +167,7 @@ export function isGlmVisionModelId(modelId: string): boolean {
|
|
|
152
167
|
* Vendor-only by design: a model's kind/variant (opus vs sonnet, codex vs base) is
|
|
153
168
|
* collapsed onto the single vendor token; use {@link parseKnownModel} for finer breakdowns.
|
|
154
169
|
*/
|
|
155
|
-
export
|
|
170
|
+
export const modelFamilyToken = memo((modelId: string): string => {
|
|
156
171
|
const parsed = parseKnownModel(modelId);
|
|
157
172
|
if (parsed.family !== "unknown") return parsed.family;
|
|
158
173
|
if (isClaudeModelId(modelId) || isAnthropicNamespacedModelId(modelId)) return "anthropic";
|
|
@@ -166,7 +181,7 @@ export function modelFamilyToken(modelId: string): string {
|
|
|
166
181
|
if (isGemmaModelId(modelId)) return "gemma";
|
|
167
182
|
if (parseGlmModel(bareModelId(modelId))) return "glm";
|
|
168
183
|
return "";
|
|
169
|
-
}
|
|
184
|
+
});
|
|
170
185
|
|
|
171
186
|
/**
|
|
172
187
|
* Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
|
|
@@ -175,23 +190,23 @@ export function modelFamilyToken(modelId: string): string {
|
|
|
175
190
|
* dashed version forms both match while bare dated ids
|
|
176
191
|
* (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
|
|
177
192
|
*/
|
|
178
|
-
export
|
|
193
|
+
export const supportsAdaptiveThinkingDisplay = memo((modelId: string): boolean => {
|
|
179
194
|
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
180
195
|
if (!parsed) return false;
|
|
181
196
|
if (isFableOrMythos(parsed.kind)) return semverGte(parsed.version, "5");
|
|
182
197
|
return parsed.kind === "opus" && semverGte(parsed.version, "4.7");
|
|
183
|
-
}
|
|
198
|
+
});
|
|
184
199
|
|
|
185
200
|
/**
|
|
186
201
|
* Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
|
|
187
202
|
* - Sampling parameters (temperature/top_p/top_k) return 400 error
|
|
188
203
|
* - Thinking content is omitted by default (needs display: "summarized")
|
|
189
204
|
*/
|
|
190
|
-
export
|
|
205
|
+
export const hasOpus47ApiRestrictions = memo((modelId: string): boolean => {
|
|
191
206
|
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
192
207
|
if (!parsed) return false;
|
|
193
208
|
return (parsed.kind === "opus" && semverGte(parsed.version, "4.7")) || isFableOrMythos(parsed.kind);
|
|
194
|
-
}
|
|
209
|
+
});
|
|
195
210
|
|
|
196
211
|
/**
|
|
197
212
|
* Mid-conversation `role: "system"` messages (system instructions appended at
|
|
@@ -200,16 +215,16 @@ export function hasOpus47ApiRestrictions(modelId: string): boolean {
|
|
|
200
215
|
* models reject the role.
|
|
201
216
|
* @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
|
|
202
217
|
*/
|
|
203
|
-
export
|
|
218
|
+
export const supportsMidConversationSystemMessages = memo((modelId: string): boolean => {
|
|
204
219
|
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
205
220
|
if (!parsed) return false;
|
|
206
221
|
return (parsed.kind === "opus" && semverGte(parsed.version, "4.8")) || isFableOrMythos(parsed.kind);
|
|
207
|
-
}
|
|
222
|
+
});
|
|
208
223
|
|
|
209
|
-
export
|
|
224
|
+
export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
|
|
210
225
|
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
211
226
|
return parsed !== null && isFableOrMythos(parsed.kind);
|
|
212
|
-
}
|
|
227
|
+
});
|
|
213
228
|
|
|
214
229
|
/** Thinking-variant token location inside a model id. */
|
|
215
230
|
export interface ThinkingVariantToken {
|
|
@@ -245,9 +260,9 @@ export function findThinkingVariantToken(modelId: string): ThinkingVariantToken
|
|
|
245
260
|
* token exists or nothing would remain. Callers MUST verify the result names
|
|
246
261
|
* a live model.
|
|
247
262
|
*/
|
|
248
|
-
export
|
|
263
|
+
export const stripThinkingVariantToken = memo((modelId: string): string | undefined => {
|
|
249
264
|
const token = findThinkingVariantToken(modelId);
|
|
250
265
|
if (!token) return undefined;
|
|
251
266
|
const stripped = modelId.slice(0, token.index) + modelId.slice(token.index + token.length);
|
|
252
267
|
return stripped.length > 0 ? stripped : undefined;
|
|
253
|
-
}
|
|
268
|
+
});
|
|
@@ -2184,6 +2184,86 @@ export function kimiCodeModelManagerOptions(
|
|
|
2184
2184
|
// 12.5. LM Studio
|
|
2185
2185
|
// ---------------------------------------------------------------------------
|
|
2186
2186
|
|
|
2187
|
+
/** Native LM Studio metadata keyed by model id from `/api/v0/models`. */
|
|
2188
|
+
export interface LmStudioNativeModelMetadata {
|
|
2189
|
+
input: ("text" | "image")[];
|
|
2190
|
+
contextWindow?: number;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/** Options for LM Studio's optional native metadata probe. */
|
|
2194
|
+
export interface LmStudioNativeModelMetadataOptions {
|
|
2195
|
+
headers?: Record<string, string>;
|
|
2196
|
+
signal?: AbortSignal;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
const LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS = 250;
|
|
2200
|
+
|
|
2201
|
+
function toLmStudioNativeBaseUrl(baseUrl: string): string {
|
|
2202
|
+
const trimmed = baseUrl.trim();
|
|
2203
|
+
const normalized = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2204
|
+
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function getLmStudioCapabilityNames(value: unknown): string[] {
|
|
2208
|
+
if (!Array.isArray(value)) {
|
|
2209
|
+
return [];
|
|
2210
|
+
}
|
|
2211
|
+
return value.flatMap(item => (typeof item === "string" ? [item.toLowerCase()] : []));
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function getLmStudioNativeInput(entry: Record<string, unknown>): ("text" | "image")[] {
|
|
2215
|
+
const modelType = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
|
|
2216
|
+
const capabilities = getLmStudioCapabilityNames(entry.capabilities);
|
|
2217
|
+
const supportsImage = modelType === "vlm" || capabilities.includes("vision") || capabilities.includes("image");
|
|
2218
|
+
return supportsImage ? ["text", "image"] : ["text"];
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function getLmStudioNativeContextWindow(entry: Record<string, unknown>): number | undefined {
|
|
2222
|
+
return (
|
|
2223
|
+
toPositiveNumber(entry.max_context_length, null) ??
|
|
2224
|
+
toPositiveNumber(entry.context_length, null) ??
|
|
2225
|
+
toPositiveNumber(entry.max_model_len, null) ??
|
|
2226
|
+
undefined
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/** Fetches LM Studio native model metadata used to mark VLM models as image-capable. */
|
|
2231
|
+
export async function fetchLmStudioNativeModelMetadata(
|
|
2232
|
+
baseUrl: string,
|
|
2233
|
+
fetchImpl: FetchImpl = fetch,
|
|
2234
|
+
options?: LmStudioNativeModelMetadataOptions,
|
|
2235
|
+
): Promise<Map<string, LmStudioNativeModelMetadata> | null> {
|
|
2236
|
+
const nativeBaseUrl = toLmStudioNativeBaseUrl(baseUrl);
|
|
2237
|
+
try {
|
|
2238
|
+
const response = await fetchImpl(`${nativeBaseUrl}/api/v0/models`, {
|
|
2239
|
+
method: "GET",
|
|
2240
|
+
headers: { Accept: "application/json", ...(options?.headers ?? {}) },
|
|
2241
|
+
signal: options?.signal ?? AbortSignal.timeout(LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS),
|
|
2242
|
+
});
|
|
2243
|
+
if (!response.ok) {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
const payload = await response.json();
|
|
2247
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
2248
|
+
return null;
|
|
2249
|
+
}
|
|
2250
|
+
const metadata = new Map<string, LmStudioNativeModelMetadata>();
|
|
2251
|
+
for (const entry of payload.data) {
|
|
2252
|
+
if (!isRecord(entry) || typeof entry.id !== "string" || entry.id.length === 0) {
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
const contextWindow = getLmStudioNativeContextWindow(entry);
|
|
2256
|
+
metadata.set(entry.id, {
|
|
2257
|
+
input: getLmStudioNativeInput(entry),
|
|
2258
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
return metadata;
|
|
2262
|
+
} catch {
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2187
2267
|
export interface LmStudioModelManagerConfig {
|
|
2188
2268
|
apiKey?: string;
|
|
2189
2269
|
baseUrl?: string;
|
|
@@ -2198,8 +2278,11 @@ export function lmStudioModelManagerOptions(
|
|
|
2198
2278
|
const references = createBundledReferenceMap<"openai-completions">("lm-studio" as any);
|
|
2199
2279
|
return {
|
|
2200
2280
|
providerId: "lm-studio",
|
|
2201
|
-
fetchDynamicModels: () =>
|
|
2202
|
-
|
|
2281
|
+
fetchDynamicModels: async () => {
|
|
2282
|
+
const nativeMetadataPromise = fetchLmStudioNativeModelMetadata(baseUrl, config?.fetch, {
|
|
2283
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
|
|
2284
|
+
});
|
|
2285
|
+
const models = await fetchOpenAICompatibleModels({
|
|
2203
2286
|
api: "openai-completions",
|
|
2204
2287
|
provider: "lm-studio",
|
|
2205
2288
|
baseUrl,
|
|
@@ -2209,7 +2292,26 @@ export function lmStudioModelManagerOptions(
|
|
|
2209
2292
|
return mapWithBundledReference(entry, defaults, reference);
|
|
2210
2293
|
},
|
|
2211
2294
|
fetch: config?.fetch,
|
|
2212
|
-
})
|
|
2295
|
+
});
|
|
2296
|
+
if (!models) {
|
|
2297
|
+
return models;
|
|
2298
|
+
}
|
|
2299
|
+
const nativeMetadata = await nativeMetadataPromise;
|
|
2300
|
+
if (!nativeMetadata) {
|
|
2301
|
+
return models;
|
|
2302
|
+
}
|
|
2303
|
+
return models.map(model => {
|
|
2304
|
+
const metadata = nativeMetadata.get(model.id);
|
|
2305
|
+
if (!metadata) {
|
|
2306
|
+
return model;
|
|
2307
|
+
}
|
|
2308
|
+
return {
|
|
2309
|
+
...model,
|
|
2310
|
+
input: metadata.input,
|
|
2311
|
+
contextWindow: metadata.contextWindow ?? model.contextWindow,
|
|
2312
|
+
};
|
|
2313
|
+
});
|
|
2314
|
+
},
|
|
2213
2315
|
};
|
|
2214
2316
|
}
|
|
2215
2317
|
|
package/src/types.ts
CHANGED
|
@@ -283,6 +283,8 @@ export interface OpenAICompat {
|
|
|
283
283
|
alwaysSendMaxTokens?: boolean;
|
|
284
284
|
/** Whether Responses-API tool-call/result history must be strictly paired. Default: auto-detected (Azure OpenAI, GitHub Copilot). */
|
|
285
285
|
strictResponsesPairing?: boolean;
|
|
286
|
+
/** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
|
|
287
|
+
supportsImageDetailOriginal?: boolean;
|
|
286
288
|
/**
|
|
287
289
|
* Append a trailing `# Juice: 0 !important` developer item when the caller
|
|
288
290
|
* did not request reasoning, suppressing default reasoning on models that
|
|
@@ -504,6 +506,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
504
506
|
| "cacheControlFormat"
|
|
505
507
|
| "thinkingKeep"
|
|
506
508
|
| "strictResponsesPairing"
|
|
509
|
+
| "supportsImageDetailOriginal"
|
|
507
510
|
| "requiresJuiceZeroHack"
|
|
508
511
|
| "enableGeminiThinkingLoopGuard"
|
|
509
512
|
| "whenThinking"
|
|
@@ -527,6 +530,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
527
530
|
export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompat {
|
|
528
531
|
supportsLongPromptCacheRetention: boolean;
|
|
529
532
|
strictResponsesPairing: boolean;
|
|
533
|
+
supportsImageDetailOriginal: boolean;
|
|
530
534
|
requiresJuiceZeroHack: boolean;
|
|
531
535
|
supportsObfuscationOptOut: boolean;
|
|
532
536
|
}
|