@dan-ai-studio/dshopencodego 0.1.43 → 0.1.44
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/README.en.md +48 -28
- package/README.md +47 -27
- package/lib/client.js +47 -1
- package/lib/index.js +321 -123
- package/lib/types/adapter.d.ts +26 -3
- package/lib/types/catalog/go-doc.d.ts +9 -5
- package/lib/types/catalog/index.d.ts +22 -7
- package/lib/types/catalog/service.d.ts +44 -1
- package/lib/types/config.d.ts +4 -3
- package/lib/types/conversion/replay.d.ts +30 -1
- package/lib/types/models.d.ts +14 -0
- package/lib/types/usage/meter.d.ts +89 -2
- package/package.json +20 -21
- package/lib/types/adapter.js +0 -384
- package/lib/types/catalog/constants.js +0 -18
- package/lib/types/catalog/contract.js +0 -147
- package/lib/types/catalog/gateway.js +0 -59
- package/lib/types/catalog/go-doc.js +0 -192
- package/lib/types/catalog/index.js +0 -377
- package/lib/types/catalog/json-response.js +0 -73
- package/lib/types/catalog/metadata.js +0 -278
- package/lib/types/catalog/protocol.js +0 -87
- package/lib/types/catalog/reading.js +0 -73
- package/lib/types/catalog/service.js +0 -45
- package/lib/types/config.js +0 -91
- package/lib/types/conversion/context.js +0 -212
- package/lib/types/conversion/index.js +0 -7
- package/lib/types/conversion/replay.js +0 -242
- package/lib/types/conversion/stream.js +0 -221
- package/lib/types/go-limits.js +0 -91
- package/lib/types/index.js +0 -198
- package/lib/types/models.js +0 -86
- package/lib/types/remotes.js +0 -29
- package/lib/types/session-header.js +0 -49
- package/lib/types/update/check.js +0 -138
- package/lib/types/update/contract.js +0 -64
- package/lib/types/update/index.js +0 -10
- package/lib/types/update/service.js +0 -39
- package/lib/types/update/version.js +0 -16
- package/lib/types/usage/contract.js +0 -106
- package/lib/types/usage/index.js +0 -8
- package/lib/types/usage/meter.js +0 -65
- package/lib/types/usage/service.js +0 -74
- package/lib/types/usage/windows.js +0 -84
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Online model metadata: how to call each advertised model.
|
|
3
|
-
*
|
|
4
|
-
* models.dev is the only source that knows about a model the day it ships, so
|
|
5
|
-
* it supplies names, capacities, modalities, pricing, lifecycle, the declared
|
|
6
|
-
* capability facts, and the protocol hint. The installed pi-ai catalog still wins where it knows the
|
|
7
|
-
* exact id, because its entries carry the wire quirks (`thinkingFormat`,
|
|
8
|
-
* reasoning-content replay, affinity format) that a generic metadata document
|
|
9
|
-
* cannot express.
|
|
10
|
-
*
|
|
11
|
-
* Every field degrades independently: a model missing a capacity falls back to
|
|
12
|
-
* the installed entry, then to the route default, and is flagged `assumedLimits`
|
|
13
|
-
* so the settings surface can say so instead of presenting a guess as a fact.
|
|
14
|
-
*
|
|
15
|
-
* @module @dan-ai-studio/dshopencodego/catalog/metadata
|
|
16
|
-
*/
|
|
17
|
-
import { asWireProtocol, decideProtocol, protocolOfNpm } from './protocol.js';
|
|
18
|
-
import { MODEL_METADATA_PROVIDER } from './constants.js';
|
|
19
|
-
/** Input modalities this build can name, in display order. */
|
|
20
|
-
export const INPUT_MODALITIES = ['text', 'image', 'audio', 'video', 'pdf'];
|
|
21
|
-
const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
22
|
-
function record(value) {
|
|
23
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
24
|
-
? value : {};
|
|
25
|
-
}
|
|
26
|
-
function positiveInteger(value) {
|
|
27
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
28
|
-
}
|
|
29
|
-
function nonEmptyString(value) {
|
|
30
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Input modalities exactly as the document declares them, in display order.
|
|
34
|
-
*
|
|
35
|
-
* A modality this build cannot name is dropped rather than passed through: the
|
|
36
|
-
* settings page labels every value it renders. An absent list, or one naming
|
|
37
|
-
* nothing recognisable, stays `undefined` — the document is silent, which is a
|
|
38
|
-
* different fact from "text only".
|
|
39
|
-
*/
|
|
40
|
-
function readInputModalities(metadata) {
|
|
41
|
-
const declared = record(metadata['modalities'])['input'];
|
|
42
|
-
if (!Array.isArray(declared))
|
|
43
|
-
return undefined;
|
|
44
|
-
const kept = INPUT_MODALITIES.filter(modality => declared.includes(modality));
|
|
45
|
-
return kept.length === 0 ? undefined : kept;
|
|
46
|
-
}
|
|
47
|
-
/** A calendar date models.dev states without a timezone. */
|
|
48
|
-
function validReleaseDate(value) {
|
|
49
|
-
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
50
|
-
return undefined;
|
|
51
|
-
return Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value ? value : undefined;
|
|
52
|
-
}
|
|
53
|
-
function rates(value) {
|
|
54
|
-
const cost = record(value);
|
|
55
|
-
const rate = (key) => {
|
|
56
|
-
const entry = cost[key];
|
|
57
|
-
return typeof entry === 'number' && Number.isFinite(entry) && entry >= 0 ? entry : 0;
|
|
58
|
-
};
|
|
59
|
-
return { input: rate('input'), output: rate('output'), cacheRead: rate('cache_read'), cacheWrite: rate('cache_write') };
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Reasoning levels a model actually offers.
|
|
63
|
-
*
|
|
64
|
-
* Only what a source states becomes a level. An absent `reasoning_options`
|
|
65
|
-
* list is not "no levels": it means the document does not describe them, so an
|
|
66
|
-
* installed entry's map is kept when there is one. A `toggle` or
|
|
67
|
-
* `budget_tokens` option says the model can think, not how the wire spells a
|
|
68
|
-
* level, so it contributes none — inventing one would put a value on the wire
|
|
69
|
-
* that the gateway may reject. Levels the model does not offer stay `null`,
|
|
70
|
-
* which is what stops the seam from offering a control the provider would
|
|
71
|
-
* ignore.
|
|
72
|
-
*/
|
|
73
|
-
function thinkingLevels(metadata, known) {
|
|
74
|
-
const options = metadata['reasoning_options'];
|
|
75
|
-
if (!Array.isArray(options))
|
|
76
|
-
return known?.thinkingLevelMap;
|
|
77
|
-
const map = Object.fromEntries(LEVELS.map(level => [level, null]));
|
|
78
|
-
for (const item of options) {
|
|
79
|
-
const option = record(item);
|
|
80
|
-
if (option['type'] !== 'effort' || !Array.isArray(option['values']))
|
|
81
|
-
continue;
|
|
82
|
-
for (const value of option['values']) {
|
|
83
|
-
const level = value === 'none' ? 'off' : value;
|
|
84
|
-
if (typeof level === 'string' && LEVELS.includes(level)) {
|
|
85
|
-
map[level] = String(value);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return map;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The closest installed entry in the same family, for inheriting wire quirks.
|
|
93
|
-
*
|
|
94
|
-
* models.dev's own `family` field is preferred when the document lists a
|
|
95
|
-
* sibling the installed catalog knows. When it does not — the common case for a
|
|
96
|
-
* model that just shipped — the longest shared id prefix stands in, which is
|
|
97
|
-
* how `deepseek-v4.1-flash` inherits `deepseek-v4-flash`'s thinking format.
|
|
98
|
-
* A shared prefix shorter than three characters is treated as no evidence.
|
|
99
|
-
*/
|
|
100
|
-
function familySibling(id, api, documentFamily, entries, builtin) {
|
|
101
|
-
if (documentFamily !== undefined) {
|
|
102
|
-
for (const [other, value] of Object.entries(entries)) {
|
|
103
|
-
if (other === id || record(value)['family'] !== documentFamily)
|
|
104
|
-
continue;
|
|
105
|
-
const candidate = builtin.get(other);
|
|
106
|
-
if (candidate !== undefined && candidate.api === api)
|
|
107
|
-
return candidate;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
let best;
|
|
111
|
-
const lower = id.toLowerCase();
|
|
112
|
-
for (const [other, candidate] of builtin) {
|
|
113
|
-
if (other === id || candidate.api !== api)
|
|
114
|
-
continue;
|
|
115
|
-
const score = commonPrefixLength(lower, other.toLowerCase());
|
|
116
|
-
if (score < 3)
|
|
117
|
-
continue;
|
|
118
|
-
if (best === undefined || score > best.score)
|
|
119
|
-
best = { model: candidate, score };
|
|
120
|
-
}
|
|
121
|
-
return best?.model;
|
|
122
|
-
}
|
|
123
|
-
function commonPrefixLength(left, right) {
|
|
124
|
-
let index = 0;
|
|
125
|
-
while (index < left.length && index < right.length && left[index] === right[index])
|
|
126
|
-
index += 1;
|
|
127
|
-
return index;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Wire quirks for one protocol.
|
|
131
|
-
*
|
|
132
|
-
* The installed entry's `compat` is authoritative when the protocol matches,
|
|
133
|
-
* including for a *family sibling*: a brand-new id in a family the catalog
|
|
134
|
-
* already serves inherits that family's quirks instead of starting from the
|
|
135
|
-
* generic defaults.
|
|
136
|
-
*/
|
|
137
|
-
function compatFor(api, metadata, exact, sibling) {
|
|
138
|
-
const inherited = exact !== undefined && exact.api === api
|
|
139
|
-
? exact.compat
|
|
140
|
-
: sibling !== undefined && sibling.api === api ? sibling.compat : undefined;
|
|
141
|
-
if (api === 'openai-completions') {
|
|
142
|
-
return {
|
|
143
|
-
supportsStore: false,
|
|
144
|
-
supportsDeveloperRole: false,
|
|
145
|
-
maxTokensField: 'max_tokens',
|
|
146
|
-
...(record(metadata['interleaved'])['field'] === 'reasoning_content'
|
|
147
|
-
? { requiresReasoningContentOnAssistantMessages: true }
|
|
148
|
-
: {}),
|
|
149
|
-
...inherited,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (api === 'openai-responses') {
|
|
153
|
-
return { sessionAffinityFormat: 'openai-nosession', ...inherited };
|
|
154
|
-
}
|
|
155
|
-
return inherited ?? {};
|
|
156
|
-
}
|
|
157
|
-
/** The base URL one protocol's SDK expects for this gateway. */
|
|
158
|
-
export function modelBaseURL(api, baseURL) {
|
|
159
|
-
const base = baseURL.replace(/\/+$/, '');
|
|
160
|
-
// The Anthropic SDK appends `/v1/messages`; the OpenAI SDKs append paths
|
|
161
|
-
// below `/v1`, which the configured base already carries.
|
|
162
|
-
return api === 'anthropic-messages' ? base.replace(/\/v1$/, '') : base;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Parse the `opencode-go` record of a models.dev document.
|
|
166
|
-
* @param body - the whole models.dev document.
|
|
167
|
-
* @param sources - installed entries, defaults, and configured overrides.
|
|
168
|
-
* @returns per-id facts and per-id parse failures; a bad entry never discards
|
|
169
|
-
* the rest of the document.
|
|
170
|
-
* @throws {Error} when the document has no usable `opencode-go` models object.
|
|
171
|
-
*/
|
|
172
|
-
export function readOnlineMetadata(body, sources) {
|
|
173
|
-
const provider = record(record(body)[MODEL_METADATA_PROVIDER]);
|
|
174
|
-
const rawModels = provider['models'];
|
|
175
|
-
if (rawModels === null || typeof rawModels !== 'object' || Array.isArray(rawModels)) {
|
|
176
|
-
throw new Error(`models.dev has no ${MODEL_METADATA_PROVIDER} models object`);
|
|
177
|
-
}
|
|
178
|
-
const providerNpm = provider['npm'];
|
|
179
|
-
const entries = record(rawModels);
|
|
180
|
-
const models = new Map();
|
|
181
|
-
const errors = new Map();
|
|
182
|
-
for (const [id, value] of Object.entries(entries)) {
|
|
183
|
-
const metadata = record(value);
|
|
184
|
-
try {
|
|
185
|
-
const exact = sources.builtin.get(id);
|
|
186
|
-
const override = asWireProtocol(sources.overrides[id]);
|
|
187
|
-
if (sources.overrides[id] !== undefined && override === undefined) {
|
|
188
|
-
throw new Error(`configured protocol "${sources.overrides[id]}" is not one of anthropic-messages, openai-completions, openai-responses`);
|
|
189
|
-
}
|
|
190
|
-
const decision = decideProtocol(id, {
|
|
191
|
-
// Guarded, not asserted: pi-ai's builtin catalog is wider than this
|
|
192
|
-
// plugin's three protocols — 1.3.0 labels `gemini-4-argon-contributor`
|
|
193
|
-
// as `google-generative-ai`. Casting it straight to `WireProtocol`
|
|
194
|
-
// let an unimplementable protocol enter as `builtin`, the most
|
|
195
|
-
// trustworthy source, and the model then failed only once a stream
|
|
196
|
-
// was attempted.
|
|
197
|
-
...asWireProtocol(exact?.api) === undefined ? {} : { builtin: asWireProtocol(exact?.api) },
|
|
198
|
-
...protocolOfNpm(metadata['provider'] === undefined ? providerNpm : record(metadata['provider'])['npm'] ?? providerNpm) === undefined
|
|
199
|
-
? {}
|
|
200
|
-
: { online: protocolOfNpm(record(metadata['provider'])['npm'] ?? providerNpm) },
|
|
201
|
-
...override === undefined ? {} : { override },
|
|
202
|
-
});
|
|
203
|
-
if (decision === undefined)
|
|
204
|
-
throw new Error('no protocol could be decided');
|
|
205
|
-
const api = decision.api;
|
|
206
|
-
const limit = record(metadata['limit']);
|
|
207
|
-
const onlineContext = positiveInteger(limit['context']);
|
|
208
|
-
const onlineInputLimit = positiveInteger(limit['input']);
|
|
209
|
-
const onlineOutput = positiveInteger(limit['output']);
|
|
210
|
-
const builtinContext = exact?.api === api ? positiveInteger(exact.contextWindow) : undefined;
|
|
211
|
-
const builtinOutput = exact?.api === api ? positiveInteger(exact.maxTokens) : undefined;
|
|
212
|
-
const contextWindow = onlineContext ?? builtinContext ?? sources.defaults.contextWindow;
|
|
213
|
-
const maxTokens = onlineOutput ?? builtinOutput ?? sources.defaults.maxTokens;
|
|
214
|
-
const onlineInput = record(metadata['modalities'])['input'];
|
|
215
|
-
const input = Array.isArray(onlineInput) && onlineInput.includes('text')
|
|
216
|
-
? (onlineInput.includes('image') ? ['text', 'image'] : ['text'])
|
|
217
|
-
: exact?.api === api ? exact.input : sources.defaults.input;
|
|
218
|
-
const reasoning = typeof metadata['reasoning'] === 'boolean'
|
|
219
|
-
? metadata['reasoning']
|
|
220
|
-
: exact?.api === api ? exact.reasoning : false;
|
|
221
|
-
const family = nonEmptyString(metadata['family']);
|
|
222
|
-
const sibling = familySibling(id, api, family, entries, sources.builtin);
|
|
223
|
-
const cost = rates(metadata['cost']);
|
|
224
|
-
const tiers = record(metadata['cost'])['tiers'];
|
|
225
|
-
if (Array.isArray(tiers)) {
|
|
226
|
-
const parsed = tiers.flatMap((item) => {
|
|
227
|
-
const tier = record(record(item)['tier']);
|
|
228
|
-
const size = positiveInteger(tier['size']);
|
|
229
|
-
return tier['type'] === 'context' && size !== undefined
|
|
230
|
-
? [{ ...rates(item), inputTokensAbove: size }]
|
|
231
|
-
: [];
|
|
232
|
-
}).sort((a, b) => a.inputTokensAbove - b.inputTokensAbove);
|
|
233
|
-
if (parsed.length > 0)
|
|
234
|
-
cost.tiers = parsed;
|
|
235
|
-
}
|
|
236
|
-
models.set(id, {
|
|
237
|
-
id,
|
|
238
|
-
name: nonEmptyString(metadata['name']) ?? id,
|
|
239
|
-
api,
|
|
240
|
-
protocolSource: decision.source,
|
|
241
|
-
contextWindow,
|
|
242
|
-
maxInputTokens: onlineInputLimit,
|
|
243
|
-
maxTokens,
|
|
244
|
-
assumedLimits: onlineContext === undefined && builtinContext === undefined
|
|
245
|
-
|| onlineOutput === undefined && builtinOutput === undefined,
|
|
246
|
-
input,
|
|
247
|
-
inputModalities: readInputModalities(metadata),
|
|
248
|
-
reasoning,
|
|
249
|
-
thinkingLevelMap: reasoning ? thinkingLevels(metadata, exact?.api === api ? exact : sibling) : undefined,
|
|
250
|
-
compat: compatFor(api, metadata, exact, sibling),
|
|
251
|
-
cost,
|
|
252
|
-
deprecated: metadata['status'] === 'deprecated',
|
|
253
|
-
releaseDate: validReleaseDate(metadata['release_date']),
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
errors.set(id, error instanceof Error ? error.message : String(error));
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return { models, errors };
|
|
261
|
-
}
|
|
262
|
-
/** Project one parsed model into the shape pi-ai's providers consume. */
|
|
263
|
-
export function toPiModel(facts, baseURL) {
|
|
264
|
-
return {
|
|
265
|
-
id: facts.id,
|
|
266
|
-
name: facts.name,
|
|
267
|
-
provider: 'opencode-go',
|
|
268
|
-
api: facts.api,
|
|
269
|
-
baseUrl: modelBaseURL(facts.api, baseURL),
|
|
270
|
-
reasoning: facts.reasoning,
|
|
271
|
-
...facts.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: facts.thinkingLevelMap },
|
|
272
|
-
input: [...facts.input],
|
|
273
|
-
contextWindow: facts.contextWindow,
|
|
274
|
-
maxTokens: facts.maxTokens,
|
|
275
|
-
cost: facts.cost,
|
|
276
|
-
compat: facts.compat,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire-protocol vocabulary and the inference ladder.
|
|
3
|
-
*
|
|
4
|
-
* The gateway is a multi-protocol front door: the same base URL answers
|
|
5
|
-
* `chat/completions`, `responses`, and `messages`, and each model speaks
|
|
6
|
-
* exactly one of them. Four evidence levels decide which, first hit wins:
|
|
7
|
-
*
|
|
8
|
-
* 1. the installed pi-ai catalog entry for that exact id (protocol *and* the
|
|
9
|
-
* wire quirks in its `compat`);
|
|
10
|
-
* 2. models.dev's per-model `provider.npm`, which names the AI SDK package the
|
|
11
|
-
* OpenCode Go console itself uses;
|
|
12
|
-
* 3. a family rule, for ids neither source describes;
|
|
13
|
-
* 4. an explicit configuration override, which always wins.
|
|
14
|
-
*
|
|
15
|
-
* Measured 2026-09-24: of the 12 gateway ids pi-ai 0.87.1 does not know, 10 are
|
|
16
|
-
* fully described by models.dev and 2 (`deepseek-flash`, `hy3-preview`) are
|
|
17
|
-
* absent from it — the family rule exists for those two and for any future id
|
|
18
|
-
* that lands the same way, not to rescue a large set.
|
|
19
|
-
*
|
|
20
|
-
* @module @dan-ai-studio/dshopencodego/catalog/protocol
|
|
21
|
-
*/
|
|
22
|
-
/** Every protocol this plugin can drive, in catalog order. */
|
|
23
|
-
export const WIRE_PROTOCOLS = [
|
|
24
|
-
'anthropic-messages',
|
|
25
|
-
'openai-completions',
|
|
26
|
-
'openai-responses',
|
|
27
|
-
];
|
|
28
|
-
/** The AI SDK package names models.dev uses to name a model's protocol. */
|
|
29
|
-
const NPM_PROTOCOLS = {
|
|
30
|
-
'@ai-sdk/anthropic': 'anthropic-messages',
|
|
31
|
-
'@ai-sdk/openai': 'openai-responses',
|
|
32
|
-
'@ai-sdk/openai-compatible': 'openai-completions',
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Families whose models are served over the OpenAI Responses API.
|
|
36
|
-
*
|
|
37
|
-
* Only families where every known member agrees are listed. Qwen and MiniMax
|
|
38
|
-
* are deliberately absent: the installed catalog serves `qwen3.8-flash` and
|
|
39
|
-
* `minimax-m3` over Anthropic Messages while serving `qwen3.6-plus`,
|
|
40
|
-
* `qwen3.7-*`, `qwen3.8-max`, and `minimax-m2.7` over Chat Completions, so a
|
|
41
|
-
* family rule would be wrong about half of them.
|
|
42
|
-
*/
|
|
43
|
-
const RESPONSES_FAMILIES = ['grok', 'gpt', 'muse'];
|
|
44
|
-
/** The protocol a models.dev `npm` field names, when it names one. */
|
|
45
|
-
export function protocolOfNpm(npm) {
|
|
46
|
-
return typeof npm === 'string' ? NPM_PROTOCOLS[npm] : undefined;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* The family rule for one id: the Responses families by prefix, everything else
|
|
50
|
-
* over Chat Completions, which is what the gateway's provider-level default
|
|
51
|
-
* (`@ai-sdk/openai-compatible`) implies for an id no per-model record covers.
|
|
52
|
-
* @param id - the gateway model id.
|
|
53
|
-
* @returns the inferred protocol; the caller records it as `inferred`.
|
|
54
|
-
*/
|
|
55
|
-
export function inferProtocol(id) {
|
|
56
|
-
const lower = id.toLowerCase();
|
|
57
|
-
return RESPONSES_FAMILIES.some(family => lower.startsWith(family))
|
|
58
|
-
? 'openai-responses'
|
|
59
|
-
: 'openai-completions';
|
|
60
|
-
}
|
|
61
|
-
/** Narrow an arbitrary configured string to a protocol this plugin can drive. */
|
|
62
|
-
export function asWireProtocol(value) {
|
|
63
|
-
return value !== undefined && WIRE_PROTOCOLS.includes(value)
|
|
64
|
-
? value
|
|
65
|
-
: undefined;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Walk the ladder for one model.
|
|
69
|
-
* @param id - the gateway model id, used only by the family rule.
|
|
70
|
-
* @param evidence - what the automatic sources and configuration say.
|
|
71
|
-
* @returns the decision, or `undefined` when the override names a protocol this
|
|
72
|
-
* build cannot drive (the caller reports that as a configuration error rather
|
|
73
|
-
* than silently falling back to a guess).
|
|
74
|
-
*/
|
|
75
|
-
export function decideProtocol(id, evidence) {
|
|
76
|
-
if (evidence.override !== undefined)
|
|
77
|
-
return { api: evidence.override, source: 'override' };
|
|
78
|
-
if (evidence.builtin !== undefined)
|
|
79
|
-
return { api: evidence.builtin, source: 'builtin' };
|
|
80
|
-
if (evidence.online !== undefined)
|
|
81
|
-
return { api: evidence.online, source: 'online' };
|
|
82
|
-
return { api: inferProtocol(id), source: 'inferred' };
|
|
83
|
-
}
|
|
84
|
-
/** The pi-ai API string for one wire protocol. */
|
|
85
|
-
export function piApiOf(api) {
|
|
86
|
-
return api;
|
|
87
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The settings-facing reading of the catalog.
|
|
3
|
-
*
|
|
4
|
-
* The settings page needs more than the picker's model list: it needs to say
|
|
5
|
-
* which models the gateway advertises, which of them this build cannot
|
|
6
|
-
* configure, how each protocol was decided, and whether the reading is stale
|
|
7
|
-
* because a refresh failed. That projection lives here as a pure function so it
|
|
8
|
-
* can be asserted without a Cordis context.
|
|
9
|
-
*
|
|
10
|
-
* @module @dan-ai-studio/dshopencodego/catalog/reading
|
|
11
|
-
*/
|
|
12
|
-
import { isModelEnabled, recommendedIds, sortModels } from '../models.js';
|
|
13
|
-
import { goQuotaFor } from '../go-limits.js';
|
|
14
|
-
/**
|
|
15
|
-
* Project one snapshot for the settings page.
|
|
16
|
-
* @param snapshot - the catalog snapshot, live or retained.
|
|
17
|
-
* @param visibility - per-model switches; absent entries keep the default.
|
|
18
|
-
* @param listingFailure - the failure message when the listing failed.
|
|
19
|
-
* @param now - the clock the "newly shipped" ordering reads; injected so a
|
|
20
|
-
* reading is a pure function of its inputs rather than of the wall clock.
|
|
21
|
-
* @returns the reading, with unconfigurable ids kept visible so the page can
|
|
22
|
-
* explain them instead of hiding them.
|
|
23
|
-
*/
|
|
24
|
-
export function catalogReading(snapshot, visibility, listingFailure, now) {
|
|
25
|
-
const documentQuotas = snapshot.documentQuotas;
|
|
26
|
-
const rows = [
|
|
27
|
-
...[...snapshot.facts.values()].map(fact => {
|
|
28
|
-
const quota = documentQuotas === undefined ? goQuotaFor(fact.id) : documentQuotas.get(fact.id);
|
|
29
|
-
const priced = fact.cost.input > 0 || fact.cost.output > 0;
|
|
30
|
-
return {
|
|
31
|
-
id: fact.id,
|
|
32
|
-
name: fact.name,
|
|
33
|
-
contextWindow: fact.contextWindow,
|
|
34
|
-
maxTokens: fact.maxTokens,
|
|
35
|
-
...fact.maxInputTokens === undefined ? {} : { maxInputTokens: fact.maxInputTokens },
|
|
36
|
-
deprecated: fact.deprecated,
|
|
37
|
-
...fact.releaseDate === undefined ? {} : { releaseDate: fact.releaseDate },
|
|
38
|
-
...quota === undefined ? {} : { goQuota: quota },
|
|
39
|
-
...!priced ? {} : {
|
|
40
|
-
cost: {
|
|
41
|
-
input: fact.cost.input,
|
|
42
|
-
output: fact.cost.output,
|
|
43
|
-
cacheRead: fact.cost.cacheRead,
|
|
44
|
-
cacheWrite: fact.cost.cacheWrite,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
protocolSource: fact.protocolSource,
|
|
48
|
-
assumedLimits: fact.assumedLimits,
|
|
49
|
-
...fact.inputModalities === undefined ? {} : { inputModalities: fact.inputModalities },
|
|
50
|
-
};
|
|
51
|
-
}),
|
|
52
|
-
...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: id, configurationMissing: reason })),
|
|
53
|
-
];
|
|
54
|
-
// Nobody configured switches? Then the default configuration keeps the top
|
|
55
|
-
// few by published monthly estimate, and every row carries that default.
|
|
56
|
-
const recommended = recommendedIds(rows);
|
|
57
|
-
const models = rows.map(row => ({ ...row, recommended: recommended.has(row.id) }));
|
|
58
|
-
const enabled = models.filter(model => isModelEnabled(model, visibility)).length;
|
|
59
|
-
return {
|
|
60
|
-
models: sortModels(models, now),
|
|
61
|
-
stale: !snapshot.live,
|
|
62
|
-
...!snapshot.live && listingFailure !== undefined ? { error: listingFailure } : {},
|
|
63
|
-
fetchedAtMs: snapshot.fetchedAtMs,
|
|
64
|
-
quotaSource: documentQuotas === undefined ? 'seed' : 'document',
|
|
65
|
-
counts: {
|
|
66
|
-
total: models.length,
|
|
67
|
-
enabled,
|
|
68
|
-
deprecated: models.filter(model => model.deprecated === true).length,
|
|
69
|
-
unconfigured: models.filter(model => model.configurationMissing !== undefined).length,
|
|
70
|
-
inferred: models.filter(model => model.protocolSource === 'inferred').length,
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host half of the catalog Remote.
|
|
3
|
-
*
|
|
4
|
-
* Both methods return the same projection; they differ only in whether the
|
|
5
|
-
* cache may answer. `refresh` is what the settings page's button calls, and it
|
|
6
|
-
* revalidates the gateway listing and the online metadata before projecting.
|
|
7
|
-
*
|
|
8
|
-
* @module @dan-ai-studio/dshopencodego/catalog/service
|
|
9
|
-
*/
|
|
10
|
-
import { RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
11
|
-
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
12
|
-
import { catalogReading } from './reading.js';
|
|
13
|
-
/** Catalog Remote: what the gateway serves, and how each model is called. */
|
|
14
|
-
export class OpencodeGoCatalogService extends TypertRemoteService {
|
|
15
|
-
options;
|
|
16
|
-
constructor(ctx, options) {
|
|
17
|
-
super(ctx, 'opencodeGoCatalog');
|
|
18
|
-
this.options = options;
|
|
19
|
-
}
|
|
20
|
-
/** The cached reading. */
|
|
21
|
-
read() {
|
|
22
|
-
return this.project(false);
|
|
23
|
-
}
|
|
24
|
-
/** Revalidate both sources, then read. */
|
|
25
|
-
refresh() {
|
|
26
|
-
return this.project(true);
|
|
27
|
-
}
|
|
28
|
-
async project(force) {
|
|
29
|
-
const catalog = this.options.catalog();
|
|
30
|
-
const snapshot = await catalog.snapshot(force);
|
|
31
|
-
const failure = snapshot.listingFailure;
|
|
32
|
-
return catalogReading(snapshot, this.options.visibility(), failure === undefined
|
|
33
|
-
? undefined
|
|
34
|
-
: failure instanceof LlmError || failure instanceof Error ? failure.message : String(failure));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/** The domain failure the settings page renders when a refresh cannot run. */
|
|
38
|
-
export function catalogFailure(error) {
|
|
39
|
-
return new RemoteError('dshopencodego/usage-unavailable', error instanceof Error
|
|
40
|
-
? error.message
|
|
41
|
-
: 'The OpenCode Go catalog is unavailable', {
|
|
42
|
-
retryable: true,
|
|
43
|
-
retainPrevious: true,
|
|
44
|
-
}, { cause: error });
|
|
45
|
-
}
|
package/lib/types/config.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration for one `dshopencodego` mount.
|
|
3
|
-
*
|
|
4
|
-
* DSH hands the plugin *live references* for every declared field, so a
|
|
5
|
-
* profile edit reaches the next operation without a restart. This plugin
|
|
6
|
-
* targets that generation only: there is no settings-document path, no
|
|
7
|
-
* `installSection` fallback, and no plain-value codec.
|
|
8
|
-
*
|
|
9
|
-
* Self-contained constraints (URL shape, numeric bounds) fail at load for the
|
|
10
|
-
* composition layer and refuse the write for a profile edit.
|
|
11
|
-
*
|
|
12
|
-
* @module @dan-ai-studio/dshopencodego/config
|
|
13
|
-
*/
|
|
14
|
-
import { RetryPolicySchema } from '@deepseek-ai/dsh-llm';
|
|
15
|
-
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
|
|
16
|
-
import z from '@deepseek-ai/schemastery';
|
|
17
|
-
import { DEFAULT_BASE_URL } from './catalog/constants.js';
|
|
18
|
-
/** Credential reference resolving the OpenCode Go API key. */
|
|
19
|
-
export const DEFAULT_API_KEY_ENV = 'OPENCODE_GO_API_KEY';
|
|
20
|
-
/** Requests and pickers share this cache lifetime; explicit discovery revalidates immediately. */
|
|
21
|
-
export const DEFAULT_REFRESH_MINUTES = 60;
|
|
22
|
-
/** Default maximum idle interval while one stream read is outstanding. */
|
|
23
|
-
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
24
|
-
/** Request-level bound on the base64-encoded image payload. */
|
|
25
|
-
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
26
|
-
/** Total-pixel budget for one request image. */
|
|
27
|
-
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 1_440_000;
|
|
28
|
-
/** Raw encoded-byte target for one request image before base64 expansion. */
|
|
29
|
-
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 400_000;
|
|
30
|
-
const fields = {
|
|
31
|
-
enabled: z.boolean().default(true),
|
|
32
|
-
modelVisibility: z.dict(z.boolean().required()).default({}),
|
|
33
|
-
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
|
|
34
|
-
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
35
|
-
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
36
|
-
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
37
|
-
maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
38
|
-
requestImagePixelBudget: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
|
|
39
|
-
requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES),
|
|
40
|
-
modelLimits: z.dict(z.union([z.const(null), z.object({
|
|
41
|
-
contextWindow: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
|
|
42
|
-
maxTokens: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
|
|
43
|
-
})])).default({}),
|
|
44
|
-
modelProtocols: z.dict(z.string()).default({}),
|
|
45
|
-
retryPolicy: RetryPolicySchema,
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* Plain resolved values used by the adapter.
|
|
49
|
-
*
|
|
50
|
-
* The input side is a partial on purpose: a profile writes only the fields it
|
|
51
|
-
* overrides, and the schema's own defaults fill the rest.
|
|
52
|
-
*/
|
|
53
|
-
export const PlainConfig = z.object(fields);
|
|
54
|
-
/** Runtime schema for {@link OpencodeGoConfig}; every field stays live. */
|
|
55
|
-
export const Config = z.object(Object.fromEntries(Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])));
|
|
56
|
-
/**
|
|
57
|
-
* Keep the Loader's references: reparsing them would detach live updates.
|
|
58
|
-
*
|
|
59
|
-
* An optional field a profile never wrote has no live reference to read, so it
|
|
60
|
-
* stays absent instead of failing the operation that asked for the config.
|
|
61
|
-
*/
|
|
62
|
-
export function readConfig(config) {
|
|
63
|
-
return Object.fromEntries(Object.keys(fields)
|
|
64
|
-
.flatMap(key => {
|
|
65
|
-
const field = config[key];
|
|
66
|
-
return field?.get === undefined ? [] : [[key, field.get()]];
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Accept only an http(s) base without a query or fragment. Runs at load for the
|
|
71
|
-
* composition layer and on every profile edit, so a bad URL fails where it is
|
|
72
|
-
* written, never at first request.
|
|
73
|
-
* @param raw - the configured base URL.
|
|
74
|
-
* @returns the normalized base URL without trailing slashes.
|
|
75
|
-
*/
|
|
76
|
-
export function assertBaseURL(raw) {
|
|
77
|
-
let url;
|
|
78
|
-
try {
|
|
79
|
-
url = new URL(raw);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
throw new Error(`dshopencodego: baseURL "${raw}" is not a valid URL`);
|
|
83
|
-
}
|
|
84
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
85
|
-
throw new Error(`dshopencodego: baseURL "${raw}" must be http or https`);
|
|
86
|
-
}
|
|
87
|
-
if (url.search.length > 0 || url.hash.length > 0) {
|
|
88
|
-
throw new Error(`dshopencodego: baseURL "${raw}" must not carry a query or fragment`);
|
|
89
|
-
}
|
|
90
|
-
return url.toString().replace(/\/+$/, '');
|
|
91
|
-
}
|