@dsh-plus/llm-pi 0.1.27 → 0.1.28
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/lib/client.js +126 -108
- package/lib/index.js +352 -147
- package/package.json +27 -27
- package/src/client/api.ts +8 -0
- package/src/client/card.tsx +3 -0
- package/src/client/constants.ts +12 -52
- package/src/client/fields.tsx +58 -0
- package/src/client/i18n.ts +2 -0
- package/src/client/views/compat.tsx +16 -1
- package/src/compat-gates.ts +265 -0
- package/src/compat.ts +70 -141
- package/src/config-api.ts +29 -0
- package/src/resolve-dsh.ts +54 -3
package/src/client/card.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '@dsh-plus/shared/client'
|
|
19
19
|
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
20
20
|
import { type ConfigValue, fetchCatalog, refreshCatalog, type WireModelsDevStatus } from './api.ts'
|
|
21
|
+
import { installCompatFields } from './constants.ts'
|
|
21
22
|
import {
|
|
22
23
|
type Draft,
|
|
23
24
|
draftFromValue,
|
|
@@ -65,6 +66,7 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
|
|
|
65
66
|
}, [value, draft])
|
|
66
67
|
|
|
67
68
|
// 运行期诊断行(kitSource / models-dev 状态)来自模型目录端点,非配置数据。
|
|
69
|
+
// compat 字段表同批下发:安装后 UI 渲染与服务端校验同源(不再手抄镜像表)。
|
|
68
70
|
useEffect(() => {
|
|
69
71
|
let alive = true
|
|
70
72
|
fetchCatalog('', 'models-dev')
|
|
@@ -72,6 +74,7 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
|
|
|
72
74
|
if (!alive) return
|
|
73
75
|
setKitSource(result.kitSource ?? null)
|
|
74
76
|
setModelsDevStatus(result.status ?? null)
|
|
77
|
+
if (result.compat !== undefined) installCompatFields(result.compat.fields)
|
|
75
78
|
})
|
|
76
79
|
.catch(() => {})
|
|
77
80
|
return () => {
|
package/src/client/constants.ts
CHANGED
|
@@ -27,69 +27,29 @@ export const TRANSPORT_OPTIONS = ['sse', 'websocket', 'websocket-cached', 'auto'
|
|
|
27
27
|
/** thinkingBudgets 档位键(来源:config.ts thinkingBudgets)。 */
|
|
28
28
|
export const BUDGET_KEYS = ['minimal', 'low', 'medium', 'high'] as const
|
|
29
29
|
|
|
30
|
-
type CompatValue = 'boolean' | 'object' | readonly string[]
|
|
30
|
+
export type CompatValue = 'boolean' | 'integer' | 'number' | 'object' | readonly string[]
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* 逐协议 compat
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* 逐协议 compat 字段表:**服务端推导结果经 /catalog 下发**,浏览器半不再手抄
|
|
34
|
+
* (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见 compat-gates.ts)。
|
|
35
|
+
* 未拿到服务端表时用 EMPTY(UI 只渲染提示行),不放内置副本兜底——宁可少画
|
|
36
|
+
* 控件,也不给出可能与官方不符的字段集。
|
|
36
37
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
supportsUsageInStreaming: 'boolean',
|
|
43
|
-
supportsFinishReason: 'boolean',
|
|
44
|
-
maxTokensField: ['max_completion_tokens', 'max_tokens'],
|
|
45
|
-
requiresToolResultName: 'boolean',
|
|
46
|
-
requiresAssistantAfterToolResult: 'boolean',
|
|
47
|
-
requiresThinkingAsText: 'boolean',
|
|
48
|
-
requiresReasoningContentOnAssistantMessages: 'boolean',
|
|
49
|
-
thinkingFormat: [
|
|
50
|
-
'openai',
|
|
51
|
-
'deepseek',
|
|
52
|
-
'openrouter',
|
|
53
|
-
'together',
|
|
54
|
-
'baseten',
|
|
55
|
-
'zai',
|
|
56
|
-
'qwen',
|
|
57
|
-
'chat-template',
|
|
58
|
-
'qwen-chat-template',
|
|
59
|
-
'string-thinking',
|
|
60
|
-
'ant-ling',
|
|
61
|
-
],
|
|
62
|
-
chatTemplateKwargs: 'object',
|
|
63
|
-
chatTemplateArgs: 'object',
|
|
64
|
-
supportsThinkingTokenBudget: 'boolean',
|
|
65
|
-
supportsStrictMode: 'boolean',
|
|
66
|
-
cacheControlFormat: ['anthropic'],
|
|
67
|
-
supportsLongCacheRetention: 'boolean',
|
|
68
|
-
},
|
|
69
|
-
'openai-responses': {
|
|
70
|
-
supportsDeveloperRole: 'boolean',
|
|
71
|
-
supportsStrictMode: 'boolean',
|
|
72
|
-
supportsLongCacheRetention: 'boolean',
|
|
73
|
-
},
|
|
74
|
-
'anthropic-messages': {
|
|
75
|
-
supportsEagerToolInputStreaming: 'boolean',
|
|
76
|
-
supportsLongCacheRetention: 'boolean',
|
|
77
|
-
supportsCacheControlOnTools: 'boolean',
|
|
78
|
-
supportsTemperature: 'boolean',
|
|
79
|
-
forceAdaptiveThinking: 'boolean',
|
|
80
|
-
allowEmptySignature: 'boolean',
|
|
81
|
-
supportsStrictTools: 'boolean',
|
|
82
|
-
},
|
|
38
|
+
let compatTable: Record<string, Record<string, CompatValue>> = {}
|
|
39
|
+
|
|
40
|
+
/** 安装服务端下发的字段表(card 挂载/刷新目录时调用)。 */
|
|
41
|
+
export function installCompatFields(table: Record<string, Record<string, CompatValue>>): void {
|
|
42
|
+
compatTable = table
|
|
83
43
|
}
|
|
84
44
|
|
|
85
45
|
/** 某协议的全部合法 compat 键(与服务端 compatFieldsOf 一致)。 */
|
|
86
46
|
export function compatFieldsOf(api: string): readonly string[] {
|
|
87
|
-
return Object.keys(
|
|
47
|
+
return Object.keys(compatTable[api] ?? {})
|
|
88
48
|
}
|
|
89
49
|
|
|
90
50
|
/** 某协议某字段的取值约束(与服务端 compatFieldSpec 一致)。 */
|
|
91
51
|
export function compatFieldSpec(api: string, field: string): CompatValue | undefined {
|
|
92
|
-
return
|
|
52
|
+
return compatTable[api]?.[field]
|
|
93
53
|
}
|
|
94
54
|
|
|
95
55
|
/** api 未设置时的渲染回退组(最常见的协议;保存仍由后端按实际协议校验)。 */
|
package/src/client/fields.tsx
CHANGED
|
@@ -84,6 +84,64 @@ export function TextField(props: TextFieldProps): ReactElement {
|
|
|
84
84
|
)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export interface IntegerFieldProps {
|
|
88
|
+
id: string
|
|
89
|
+
label: string
|
|
90
|
+
hint?: string
|
|
91
|
+
invalidLabel: string
|
|
92
|
+
value: unknown
|
|
93
|
+
/** 放弃/保存后外部重置草稿时自增,驱动本地文本重新播种。 */
|
|
94
|
+
epoch: number
|
|
95
|
+
disabled?: boolean
|
|
96
|
+
wide?: boolean
|
|
97
|
+
/** 合法整数回调整数;空输入与非法输入回调 undefined。 */
|
|
98
|
+
onEdit(value: unknown): void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 整数字段:compat 的数值型开关(如 vllmPriority,官方 z.number().step(1))。
|
|
103
|
+
* 本地保留文本态(与 JsonField 同款 epoch 播种),仅在解析为整数时回写草稿——
|
|
104
|
+
* 小数/非数字显示错误且不提交,与服务端 checkValue 的整数语义一致
|
|
105
|
+
* (后端仍做最终校验)。
|
|
106
|
+
*/
|
|
107
|
+
export function IntegerField(props: IntegerFieldProps): ReactElement {
|
|
108
|
+
const [text, setText] = useState(() => (props.value === undefined ? '' : String(props.value)))
|
|
109
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: 仅在 epoch 递增(外部重置语义)时同步文本,避免编辑中被 value 回写打断
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
setText(props.value === undefined ? '' : String(props.value))
|
|
112
|
+
}, [props.epoch])
|
|
113
|
+
const trimmed = text.trim()
|
|
114
|
+
const parsed = trimmed === '' ? undefined : Number(trimmed)
|
|
115
|
+
const ok = trimmed === '' || (parsed !== undefined && Number.isInteger(parsed))
|
|
116
|
+
return (
|
|
117
|
+
<div className={`lpc-field${props.wide === true ? ' lpc-wide' : ''}`}>
|
|
118
|
+
<div className="lpc-head">
|
|
119
|
+
<label className="lpc-label" htmlFor={props.id}>
|
|
120
|
+
{props.label}
|
|
121
|
+
</label>
|
|
122
|
+
</div>
|
|
123
|
+
<input
|
|
124
|
+
id={props.id}
|
|
125
|
+
className={`lpc-input${ok ? '' : ' lpc-inputInvalid'}`}
|
|
126
|
+
type="text"
|
|
127
|
+
inputMode="numeric"
|
|
128
|
+
aria-invalid={ok ? undefined : true}
|
|
129
|
+
value={text}
|
|
130
|
+
disabled={props.disabled === true}
|
|
131
|
+
onChange={(event) => {
|
|
132
|
+
const next = event.target.value
|
|
133
|
+
setText(next)
|
|
134
|
+
const value = next.trim() === '' ? undefined : Number(next.trim())
|
|
135
|
+
props.onEdit(value !== undefined && Number.isInteger(value) ? value : undefined)
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<p className={ok ? 'lpc-hint' : 'lpc-invalid'}>
|
|
139
|
+
{ok ? (props.hint ?? '') : props.invalidLabel}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
87
145
|
export interface SelectFieldProps {
|
|
88
146
|
id: string
|
|
89
147
|
label: string
|
package/src/client/i18n.ts
CHANGED
|
@@ -62,6 +62,7 @@ const ownZh = {
|
|
|
62
62
|
retryPolicy: '重试策略(JSON)',
|
|
63
63
|
retryPolicyHint: 'dsh-llm RetryPolicy 形状;非法 JSON 不会提交。',
|
|
64
64
|
invalidJson: 'JSON 格式错误(该字段不会提交)。',
|
|
65
|
+
invalidInteger: '必须是整数(该字段不会提交)。',
|
|
65
66
|
compatGroup: 'Compat 覆盖',
|
|
66
67
|
compatApiHint: 'api 未设置时暂按 openai-completions 字段组渲染;保存时后端按实际协议校验。',
|
|
67
68
|
compatUnset: '未设置',
|
|
@@ -144,6 +145,7 @@ const ownEn = {
|
|
|
144
145
|
retryPolicy: 'Retry policy (JSON)',
|
|
145
146
|
retryPolicyHint: 'dsh-llm RetryPolicy shape; invalid JSON is not submitted.',
|
|
146
147
|
invalidJson: 'Invalid JSON (this field will not be submitted).',
|
|
148
|
+
invalidInteger: 'Must be an integer (this field will not be submitted).',
|
|
147
149
|
compatGroup: 'Compat overrides',
|
|
148
150
|
compatApiHint:
|
|
149
151
|
'When api is unset, fields render per openai-completions; the backend validates per the effective protocol.',
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { ReactElement } from 'react'
|
|
9
9
|
|
|
10
10
|
import { COMPAT_FALLBACK_API, compatFieldSpec, compatFieldsOf } from '../constants.ts'
|
|
11
|
-
import { CollapseSection, JsonField, SelectField } from '../fields.tsx'
|
|
11
|
+
import { CollapseSection, IntegerField, JsonField, SelectField } from '../fields.tsx'
|
|
12
12
|
import type { Translate } from '../i18n.ts'
|
|
13
13
|
|
|
14
14
|
/** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
|
|
@@ -90,6 +90,21 @@ export function CompatEditor(props: CompatEditorProps): ReactElement {
|
|
|
90
90
|
/>
|
|
91
91
|
)
|
|
92
92
|
}
|
|
93
|
+
if (spec === 'integer' || spec === 'number') {
|
|
94
|
+
return (
|
|
95
|
+
<IntegerField
|
|
96
|
+
key={field}
|
|
97
|
+
id={`${props.idPrefix}-${field}`}
|
|
98
|
+
label={field}
|
|
99
|
+
value={props.compat[field]}
|
|
100
|
+
epoch={props.epoch}
|
|
101
|
+
invalidLabel={props.t('invalidInteger')}
|
|
102
|
+
disabled={props.disabled === true}
|
|
103
|
+
onEdit={(value) => setField(field, value)}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
// 余下只有枚举(readonly string[]):boolean/object/number 已在上方分支消化
|
|
93
108
|
return (
|
|
94
109
|
<SelectField
|
|
95
110
|
key={field}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compat 门控表的**自动继承**:从官方 `dsh-llm-pi-ai` 安装副本现场推导,
|
|
3
|
+
* 取代早期的手抄镜像表(手抄遗漏即静默误拒官方可配字段,见 ADR/文档的
|
|
4
|
+
* 0.1.5-rc.1 教训)。
|
|
5
|
+
*
|
|
6
|
+
* 官方不导出该表(包根仅 7 个符号、`src/` 不随 npm 发布),但有两条稳定可用的
|
|
7
|
+
* 机器可读来源,本模块同时消费:
|
|
8
|
+
*
|
|
9
|
+
* 1. **运行期 bundle**(`lib/index.js`,即真正执行门控的那份代码):
|
|
10
|
+
* `const COMPAT_GATES = { "openai-completions": COMPLETIONS_COMPAT_GATE, ... }`
|
|
11
|
+
* ——给出「协议 → 门控」映射(anthropic 在 bundle 内联,解析器同时支持
|
|
12
|
+
* 命名常量与内联对象两种形态)。门控表取自这里而非 d.ts,因为它是**事实源**:
|
|
13
|
+
* 运行期拒绝哪些字段由它决定。
|
|
14
|
+
* 2. **导出的 Config schema**(schemastery 标准 schema):`providers.*.compat`
|
|
15
|
+
* 节点逐字段给出取值约束(boolean/number.step(1)/enum/对象),据此推导
|
|
16
|
+
* 取值校验,不再手写 VALUE_SPECS。
|
|
17
|
+
*
|
|
18
|
+
* 推导失败(官方改布局/改打包形态)时回退到 FALLBACK_TABLE(最后已知快照,
|
|
19
|
+
* 随本文件版本冻结)并给出诊断——绝不因推导失败弄挂插件启动。
|
|
20
|
+
* @module llm-pi/compat-gates
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** 字段可配性:offer = 官方允许写;withhold = 官方为厂商内置、写时拒绝。 */
|
|
24
|
+
export type CompatDisposition = 'offer' | 'withhold'
|
|
25
|
+
|
|
26
|
+
/** 取值约束(从官方 schema 节点推导;'integer' = number 且 step=1)。 */
|
|
27
|
+
export type CompatValue = 'boolean' | 'integer' | 'number' | 'object' | readonly string[]
|
|
28
|
+
|
|
29
|
+
/** 推导结果:协议 → (字段 → 分型) + 字段 → 取值约束。 */
|
|
30
|
+
export interface CompatTable {
|
|
31
|
+
readonly gates: Readonly<Record<string, Readonly<Record<string, CompatDisposition>>>>
|
|
32
|
+
readonly specs: Readonly<Record<string, CompatValue>>
|
|
33
|
+
/** 数据来源(诊断/状态行):official = 现场推导成功。 */
|
|
34
|
+
readonly source: 'official' | 'fallback'
|
|
35
|
+
/** source=fallback 时的原因(供日志)。 */
|
|
36
|
+
readonly problem?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 官方 Config schema 的最小结构面(schemastery 节点;只读我们消费的部分)。 */
|
|
40
|
+
export interface SchemaNode {
|
|
41
|
+
type?: string
|
|
42
|
+
dict?: Record<string, SchemaNode>
|
|
43
|
+
list?: readonly SchemaNode[]
|
|
44
|
+
inner?: SchemaNode
|
|
45
|
+
meta?: { step?: number }
|
|
46
|
+
value?: unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 官方插件模块表面(推导所需的两个输入)。 */
|
|
50
|
+
export interface OfficialAdapterSurface {
|
|
51
|
+
/** `lib/index.js` 的绝对路径(读文本解析 COMPAT_GATES)。 */
|
|
52
|
+
bundlePath: string
|
|
53
|
+
/** 导入的模块命名空间(取 Config 导出)。 */
|
|
54
|
+
module: Record<string, unknown>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 从 `{` 起做括号配对,返回内部文本;未配对返回 undefined。 */
|
|
58
|
+
function balancedBody(text: string, openIndex: number): string | undefined {
|
|
59
|
+
const start = text.indexOf('{', openIndex)
|
|
60
|
+
if (start < 0) return undefined
|
|
61
|
+
let depth = 0
|
|
62
|
+
for (let i = start; i < text.length; i += 1) {
|
|
63
|
+
const ch = text[i]
|
|
64
|
+
if (ch === '{') depth += 1
|
|
65
|
+
else if (ch === '}') {
|
|
66
|
+
depth -= 1
|
|
67
|
+
if (depth === 0) return text.slice(start + 1, i)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 解析 `field: "offer" | "withhold"` 键值对。 */
|
|
74
|
+
function parseGateFields(body: string): Record<string, CompatDisposition> {
|
|
75
|
+
const out: Record<string, CompatDisposition> = {}
|
|
76
|
+
for (const m of body.matchAll(/(\w+):\s*"(offer|withhold)"/g)) {
|
|
77
|
+
out[m[1] as string] = m[2] as CompatDisposition
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 取 `const NAME = { ... }` 的块体(兼容 `let`/`var` 与省略分号)。 */
|
|
83
|
+
function namedBlock(bundle: string, name: string): string | undefined {
|
|
84
|
+
const re = new RegExp(`(?:const|let|var)\\s+${name}\\s*=\\s*`)
|
|
85
|
+
const m = re.exec(bundle)
|
|
86
|
+
return m === null ? undefined : balancedBody(bundle, m.index + m[0].length)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 解析顶层 `"api": IDENT` 与 `"api": { ... }` 两种条目。 */
|
|
90
|
+
function splitGateEntries(body: string): Map<string, string | Record<string, CompatDisposition>> {
|
|
91
|
+
const out = new Map<string, string | Record<string, CompatDisposition>>()
|
|
92
|
+
const re = /"([^"]+)"\s*:\s*/g
|
|
93
|
+
let m: RegExpExecArray | null = re.exec(body)
|
|
94
|
+
while (m !== null) {
|
|
95
|
+
const api = m[1] as string
|
|
96
|
+
const rest = body.slice(m.index + m[0].length)
|
|
97
|
+
if (rest.startsWith('{')) {
|
|
98
|
+
const inner = balancedBody(rest, 0)
|
|
99
|
+
if (inner !== undefined) out.set(api, parseGateFields(inner))
|
|
100
|
+
} else {
|
|
101
|
+
const ident = /^[A-Za-z_$][\w$]*/.exec(rest)
|
|
102
|
+
if (ident !== null) out.set(api, ident[0])
|
|
103
|
+
}
|
|
104
|
+
m = re.exec(body)
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 从运行期 bundle 推导「协议 → 门控」:解析 COMPAT_GATES 的每条目,
|
|
111
|
+
* 命名常量回查同名 `const` 块,内联对象直接取字段。
|
|
112
|
+
*/
|
|
113
|
+
export function deriveGates(
|
|
114
|
+
bundle: string,
|
|
115
|
+
): Record<string, Record<string, CompatDisposition>> | undefined {
|
|
116
|
+
const gatesBody = namedBlock(bundle, 'COMPAT_GATES')
|
|
117
|
+
if (gatesBody === undefined) return undefined
|
|
118
|
+
const out: Record<string, Record<string, CompatDisposition>> = {}
|
|
119
|
+
for (const [api, ref] of splitGateEntries(gatesBody)) {
|
|
120
|
+
if (typeof ref !== 'string') {
|
|
121
|
+
out[api] = ref
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
const block = namedBlock(bundle, ref)
|
|
125
|
+
if (block === undefined) continue
|
|
126
|
+
const fields = parseGateFields(block)
|
|
127
|
+
if (Object.keys(fields).length > 0) out[api] = fields
|
|
128
|
+
}
|
|
129
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 单个 schema 节点 → 取值约束;无法归类的返回 undefined(跳过该字段)。 */
|
|
133
|
+
function specOfNode(node: SchemaNode | undefined): CompatValue | undefined {
|
|
134
|
+
if (node === undefined) return undefined
|
|
135
|
+
if (node.type === 'boolean') return 'boolean'
|
|
136
|
+
if (node.type === 'number') return node.meta?.step === 1 ? 'integer' : 'number'
|
|
137
|
+
if (node.type === 'dict') return 'object'
|
|
138
|
+
if (node.type === 'object') return 'object'
|
|
139
|
+
if (node.type === 'union' && Array.isArray(node.list) && node.list.length > 0) {
|
|
140
|
+
const values: string[] = []
|
|
141
|
+
for (const member of node.list) {
|
|
142
|
+
if (member?.type === 'const' && typeof member.value === 'string') values.push(member.value)
|
|
143
|
+
else return undefined // 含非字符串成员的联合(如模板 $var 对象)不入枚举
|
|
144
|
+
}
|
|
145
|
+
return values
|
|
146
|
+
}
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 从官方 Config schema 推导字段取值约束(`providers.*.compat` 节点)。 */
|
|
151
|
+
export function deriveSpecs(module: Record<string, unknown>): Record<string, CompatValue> {
|
|
152
|
+
const config = module['Config'] as SchemaNode | undefined
|
|
153
|
+
const compat = config?.dict?.['providers']?.inner?.dict?.['compat']
|
|
154
|
+
const fields = compat?.dict
|
|
155
|
+
if (fields === undefined) return {}
|
|
156
|
+
const out: Record<string, CompatValue> = {}
|
|
157
|
+
for (const [field, node] of Object.entries(fields)) {
|
|
158
|
+
const spec = specOfNode(node)
|
|
159
|
+
if (spec !== undefined) out[field] = spec
|
|
160
|
+
}
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 官方门控表不可解析时的最后已知快照(0.1.5-rc.2 实测值)。 */
|
|
165
|
+
export const FALLBACK_TABLE: CompatTable = {
|
|
166
|
+
source: 'fallback',
|
|
167
|
+
gates: {
|
|
168
|
+
'openai-completions': {
|
|
169
|
+
supportsStore: 'offer',
|
|
170
|
+
supportsDeveloperRole: 'offer',
|
|
171
|
+
supportsReasoningEffort: 'offer',
|
|
172
|
+
supportsUsageInStreaming: 'offer',
|
|
173
|
+
supportsFinishReason: 'offer',
|
|
174
|
+
maxTokensField: 'offer',
|
|
175
|
+
requiresToolResultName: 'offer',
|
|
176
|
+
requiresAssistantAfterToolResult: 'offer',
|
|
177
|
+
requiresThinkingAsText: 'offer',
|
|
178
|
+
requiresReasoningContentOnAssistantMessages: 'offer',
|
|
179
|
+
thinkingFormat: 'offer',
|
|
180
|
+
chatTemplateKwargs: 'offer',
|
|
181
|
+
chatTemplateArgs: 'offer',
|
|
182
|
+
supportsThinkingTokenBudget: 'offer',
|
|
183
|
+
thinkingTokenBudgetField: 'offer',
|
|
184
|
+
vllmPriority: 'offer',
|
|
185
|
+
supportsStrictMode: 'offer',
|
|
186
|
+
cacheControlFormat: 'offer',
|
|
187
|
+
supportsLongCacheRetention: 'offer',
|
|
188
|
+
openRouterRouting: 'withhold',
|
|
189
|
+
vercelGatewayRouting: 'withhold',
|
|
190
|
+
zaiToolStream: 'withhold',
|
|
191
|
+
supportsOpenAIGrammarTools: 'withhold',
|
|
192
|
+
sendSessionAffinityHeaders: 'withhold',
|
|
193
|
+
deferredToolsMode: 'withhold',
|
|
194
|
+
sessionAffinityFormat: 'withhold',
|
|
195
|
+
},
|
|
196
|
+
'openai-responses': {
|
|
197
|
+
supportsDeveloperRole: 'offer',
|
|
198
|
+
supportsMaxOutputTokens: 'offer',
|
|
199
|
+
supportsStrictMode: 'offer',
|
|
200
|
+
supportsLongCacheRetention: 'offer',
|
|
201
|
+
sessionAffinityFormat: 'withhold',
|
|
202
|
+
supportsOpenAIGrammarTools: 'withhold',
|
|
203
|
+
supportsAdditionalTools: 'withhold',
|
|
204
|
+
supportsToolSearch: 'withhold',
|
|
205
|
+
supportsExplicitPromptCacheMode: 'withhold',
|
|
206
|
+
},
|
|
207
|
+
'anthropic-messages': {
|
|
208
|
+
supportsEagerToolInputStreaming: 'offer',
|
|
209
|
+
supportsLongCacheRetention: 'offer',
|
|
210
|
+
supportsCacheControlOnTools: 'offer',
|
|
211
|
+
supportsTemperature: 'offer',
|
|
212
|
+
forceAdaptiveThinking: 'offer',
|
|
213
|
+
allowEmptySignature: 'offer',
|
|
214
|
+
supportsStrictTools: 'offer',
|
|
215
|
+
sendSessionAffinityHeaders: 'withhold',
|
|
216
|
+
supportsToolReferences: 'withhold',
|
|
217
|
+
supportsMidConvoEffort: 'withhold',
|
|
218
|
+
allowedFallbackModels: 'withhold',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
specs: {
|
|
222
|
+
supportsStore: 'boolean',
|
|
223
|
+
supportsDeveloperRole: 'boolean',
|
|
224
|
+
supportsReasoningEffort: 'boolean',
|
|
225
|
+
supportsUsageInStreaming: 'boolean',
|
|
226
|
+
supportsFinishReason: 'boolean',
|
|
227
|
+
maxTokensField: ['max_completion_tokens', 'max_tokens'],
|
|
228
|
+
requiresToolResultName: 'boolean',
|
|
229
|
+
requiresAssistantAfterToolResult: 'boolean',
|
|
230
|
+
requiresThinkingAsText: 'boolean',
|
|
231
|
+
requiresReasoningContentOnAssistantMessages: 'boolean',
|
|
232
|
+
thinkingFormat: [
|
|
233
|
+
'openai',
|
|
234
|
+
'deepseek',
|
|
235
|
+
'openrouter',
|
|
236
|
+
'together',
|
|
237
|
+
'baseten',
|
|
238
|
+
'zai',
|
|
239
|
+
'qwen',
|
|
240
|
+
'chat-template',
|
|
241
|
+
'qwen-chat-template',
|
|
242
|
+
'string-thinking',
|
|
243
|
+
'ant-ling',
|
|
244
|
+
],
|
|
245
|
+
chatTemplateKwargs: 'object',
|
|
246
|
+
chatTemplateArgs: 'object',
|
|
247
|
+
supportsThinkingTokenBudget: 'boolean',
|
|
248
|
+
thinkingTokenBudgetField: [
|
|
249
|
+
'thinking_token_budget',
|
|
250
|
+
'thinking_budget',
|
|
251
|
+
'thinking_budget_tokens',
|
|
252
|
+
],
|
|
253
|
+
vllmPriority: 'integer',
|
|
254
|
+
supportsMaxOutputTokens: 'boolean',
|
|
255
|
+
supportsStrictMode: 'boolean',
|
|
256
|
+
cacheControlFormat: ['anthropic'],
|
|
257
|
+
supportsLongCacheRetention: 'boolean',
|
|
258
|
+
supportsEagerToolInputStreaming: 'boolean',
|
|
259
|
+
supportsCacheControlOnTools: 'boolean',
|
|
260
|
+
supportsTemperature: 'boolean',
|
|
261
|
+
forceAdaptiveThinking: 'boolean',
|
|
262
|
+
allowEmptySignature: 'boolean',
|
|
263
|
+
supportsStrictTools: 'boolean',
|
|
264
|
+
},
|
|
265
|
+
}
|