@ebony-vinyl/dsh-our-free-model 2.8.0 → 2.8.2
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.md +12 -7
- package/client.js +894 -6
- package/index.js +245 -8
- package/package.json +4 -1
- package/src/adapter.js +54 -6
- package/src/aggregate.js +53 -13
- package/src/bun-fetch.js +33 -10
- package/src/catalog.js +140 -16
- package/src/core.js +122 -33
- package/src/egress.js +204 -3
- package/src/engine-parts/win32-x64.000.part +0 -0
- package/src/engine-parts/win32-x64.001.part +0 -0
- package/src/engine-parts/win32-x64.002.part +0 -0
- package/src/engine-parts/win32-x64.003.part +0 -0
- package/src/engine-parts/win32-x64.004.part +0 -0
- package/src/engine-parts/win32-x64.005.part +0 -0
- package/src/engine-parts/win32-x64.006.part +0 -0
- package/src/extensions.js +1888 -0
- package/src/feed.js +6 -0
- package/src/http.js +14 -3
- package/src/kilo.js +55 -5
- package/src/magpie-engine.js +56 -2
- package/src/magpie-models.js +31 -3
- package/src/model-test.js +3 -1
- package/src/o2a.js +207 -8
- package/src/open-external.js +43 -5
- package/src/plugins.js +1240 -0
- package/src/providers/registry.json +72 -30
- package/src/semver.js +40 -0
- package/src/store.js +17 -1
- package/src/stream.js +39 -4
- package/src/subscription-completion.js +11 -2
- package/src/updater.js +5 -33
- package/src/upstream.js +43 -0
- package/vendor/channel-pack/NOTICE.md +1 -1
- package/vendor/channel-pack/pack.js +7242 -3952
- package/vendor/magpie/NOTICE.md +7 -2
- package/vendor/magpie/engine.json +18 -12
|
@@ -3827,63 +3827,105 @@
|
|
|
3827
3827
|
"env": "KILO_API_KEY",
|
|
3828
3828
|
"url": "https://app.kilo.ai"
|
|
3829
3829
|
},
|
|
3830
|
-
"baseUrl": "https://
|
|
3830
|
+
"baseUrl": "https://ai-gateway.kilo.ai/api/v1",
|
|
3831
3831
|
"format": "openai-chat",
|
|
3832
3832
|
"models": [
|
|
3833
3833
|
{
|
|
3834
3834
|
"id": "kilo-auto/free",
|
|
3835
|
-
"name": "
|
|
3835
|
+
"name": "Auto Free",
|
|
3836
3836
|
"contextWindow": 256000,
|
|
3837
|
-
"vision":
|
|
3838
|
-
"reasoning":
|
|
3837
|
+
"vision": false,
|
|
3838
|
+
"reasoning": true
|
|
3839
3839
|
},
|
|
3840
3840
|
{
|
|
3841
|
-
"id": "nvidia/nemotron-3-super-
|
|
3842
|
-
"name": "Nemotron 3 Super
|
|
3843
|
-
"contextWindow":
|
|
3844
|
-
"vision":
|
|
3845
|
-
"reasoning":
|
|
3841
|
+
"id": "nvidia/nemotron-3.5-super-vl:free",
|
|
3842
|
+
"name": "NVIDIA: Nemotron 3.5 Super VL",
|
|
3843
|
+
"contextWindow": 1000000,
|
|
3844
|
+
"vision": true,
|
|
3845
|
+
"reasoning": true
|
|
3846
3846
|
},
|
|
3847
3847
|
{
|
|
3848
|
-
"id": "
|
|
3849
|
-
"name": "
|
|
3850
|
-
"contextWindow":
|
|
3851
|
-
"vision":
|
|
3852
|
-
"reasoning":
|
|
3848
|
+
"id": "reflection/beam-free",
|
|
3849
|
+
"name": "Reflection: Beam 1.0",
|
|
3850
|
+
"contextWindow": 1048576,
|
|
3851
|
+
"vision": false,
|
|
3852
|
+
"reasoning": true
|
|
3853
3853
|
},
|
|
3854
3854
|
{
|
|
3855
|
-
"id": "
|
|
3856
|
-
"name": "
|
|
3855
|
+
"id": "stealth/glyph-cluster",
|
|
3856
|
+
"name": "Stealth: Glyph Cluster",
|
|
3857
3857
|
"contextWindow": 256000,
|
|
3858
|
-
"vision":
|
|
3859
|
-
"reasoning":
|
|
3858
|
+
"vision": false,
|
|
3859
|
+
"reasoning": true
|
|
3860
3860
|
},
|
|
3861
3861
|
{
|
|
3862
|
-
"id": "
|
|
3863
|
-
"name": "
|
|
3862
|
+
"id": "stealth/crouton-alpha",
|
|
3863
|
+
"name": "Crouton Alpha",
|
|
3864
|
+
"contextWindow": 1048576,
|
|
3865
|
+
"vision": true,
|
|
3866
|
+
"reasoning": true
|
|
3867
|
+
},
|
|
3868
|
+
{
|
|
3869
|
+
"id": "dots-studio/dots-3-note-preview:free",
|
|
3870
|
+
"name": "Dots Studio: Dots3-Note Preview",
|
|
3871
|
+
"contextWindow": 512000,
|
|
3872
|
+
"vision": true,
|
|
3873
|
+
"reasoning": true
|
|
3874
|
+
},
|
|
3875
|
+
{
|
|
3876
|
+
"id": "nvidia/nemotron-3.5-lightning:free",
|
|
3877
|
+
"name": "NVIDIA: Nemotron 3.5 Lightning",
|
|
3864
3878
|
"contextWindow": 1000000,
|
|
3865
|
-
"vision":
|
|
3866
|
-
"reasoning":
|
|
3879
|
+
"vision": false,
|
|
3880
|
+
"reasoning": true
|
|
3867
3881
|
},
|
|
3868
3882
|
{
|
|
3869
|
-
"id": "
|
|
3870
|
-
"name": "
|
|
3883
|
+
"id": "cohere/north-mini-code:free",
|
|
3884
|
+
"name": "Cohere: North Mini Code",
|
|
3885
|
+
"contextWindow": 256000,
|
|
3886
|
+
"vision": false,
|
|
3887
|
+
"reasoning": true
|
|
3888
|
+
},
|
|
3889
|
+
{
|
|
3890
|
+
"id": "nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
3891
|
+
"name": "NVIDIA: Nemotron 3 Ultra",
|
|
3871
3892
|
"contextWindow": 1000000,
|
|
3872
|
-
"vision":
|
|
3873
|
-
"reasoning":
|
|
3893
|
+
"vision": false,
|
|
3894
|
+
"reasoning": true
|
|
3895
|
+
},
|
|
3896
|
+
{
|
|
3897
|
+
"id": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
|
3898
|
+
"name": "NVIDIA: Nemotron 3 Nano Omni",
|
|
3899
|
+
"contextWindow": 256000,
|
|
3900
|
+
"vision": true,
|
|
3901
|
+
"reasoning": true
|
|
3902
|
+
},
|
|
3903
|
+
{
|
|
3904
|
+
"id": "openrouter/free",
|
|
3905
|
+
"name": "OpenRouter Free Models Router",
|
|
3906
|
+
"contextWindow": 200000,
|
|
3907
|
+
"vision": true,
|
|
3908
|
+
"reasoning": true
|
|
3874
3909
|
}
|
|
3875
3910
|
],
|
|
3876
|
-
"notes": "Kilo gateway free pool; the plugin’s own Kilo channel
|
|
3911
|
+
"notes": "Kilo gateway free pool, snapshot 2026-11-07; the plugin’s own Kilo channel serves it keyless from the live listing — this row is the aggregate-lane fallback for a pasted key.",
|
|
3877
3912
|
"website": "https://kilo.ai/",
|
|
3878
3913
|
"local": false,
|
|
3879
3914
|
"source": "fcc",
|
|
3880
3915
|
"freeModelsHint": [
|
|
3881
3916
|
"kilo-auto/free",
|
|
3882
|
-
"nvidia/nemotron-3-super-
|
|
3917
|
+
"nvidia/nemotron-3.5-super-vl:free",
|
|
3918
|
+
"reflection/beam-free",
|
|
3919
|
+
"stealth/glyph-cluster",
|
|
3920
|
+
"stealth/crouton-alpha",
|
|
3921
|
+
"dots-studio/dots-3-note-preview:free",
|
|
3922
|
+
"nvidia/nemotron-3.5-lightning:free",
|
|
3923
|
+
"cohere/north-mini-code:free",
|
|
3883
3924
|
"nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
3884
|
-
"
|
|
3925
|
+
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
|
3926
|
+
"openrouter/free"
|
|
3885
3927
|
],
|
|
3886
|
-
"modelsUrl": "https://
|
|
3928
|
+
"modelsUrl": "https://ai-gateway.kilo.ai/api/v1/models"
|
|
3887
3929
|
},
|
|
3888
3930
|
{
|
|
3889
3931
|
"id": "kimchi",
|
package/src/semver.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semver comparison shared by the self-updater and the extension platform.
|
|
3
|
+
* Lives apart from updater.js because the standalone package deliberately
|
|
4
|
+
* ships without the updater — the extension runtime still needs to compare a
|
|
5
|
+
* registry entry's minHost against the host version.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Parse `1.2.3` / `1.2.3-rc.4` into a comparable tuple; `null` when malformed. */
|
|
9
|
+
export function parseVersion(value) {
|
|
10
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?$/.exec(String(value ?? '').trim())
|
|
11
|
+
if (match === null) return null
|
|
12
|
+
const pre = match[4] === undefined ? null : match[4].split('.')
|
|
13
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), pre }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** -1 | 0 | 1 in semver precedence; a pre-release sorts before its release. */
|
|
17
|
+
export function compareVersions(a, b) {
|
|
18
|
+
const left = parseVersion(a)
|
|
19
|
+
const right = parseVersion(b)
|
|
20
|
+
if (left === null || right === null) return a === b ? 0 : a > b ? 1 : -1
|
|
21
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
22
|
+
if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1
|
|
23
|
+
}
|
|
24
|
+
if (left.pre === null && right.pre === null) return 0
|
|
25
|
+
if (left.pre === null) return 1
|
|
26
|
+
if (right.pre === null) return -1
|
|
27
|
+
const width = Math.max(left.pre.length, right.pre.length)
|
|
28
|
+
for (let i = 0; i < width; i += 1) {
|
|
29
|
+
const x = left.pre[i]
|
|
30
|
+
const y = right.pre[i]
|
|
31
|
+
if (x === undefined) return -1
|
|
32
|
+
if (y === undefined) return 1
|
|
33
|
+
const xn = /^\d+$/.test(x)
|
|
34
|
+
const yn = /^\d+$/.test(y)
|
|
35
|
+
if (xn && yn && Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
|
|
36
|
+
if (xn !== yn) return xn ? -1 : 1
|
|
37
|
+
if (x !== y) return x < y ? -1 : 1
|
|
38
|
+
}
|
|
39
|
+
return 0
|
|
40
|
+
}
|
package/src/store.js
CHANGED
|
@@ -159,6 +159,15 @@ export const SETTINGS_INITIAL = {
|
|
|
159
159
|
version: 1,
|
|
160
160
|
/** Master switch: when off the routes stay registered but serve nothing. */
|
|
161
161
|
enabled: true,
|
|
162
|
+
/** Allow installing unsigned local plugin packages from disk. Off by
|
|
163
|
+
* default: the signed marketplace is the trust root, and a local package is
|
|
164
|
+
* an explicit developer action with its own warning and badge. */
|
|
165
|
+
developerPlugins: false,
|
|
166
|
+
/** Install extension updates automatically once the registry tick runs and
|
|
167
|
+
* the newer version has soaked past its supply-chain window (see
|
|
168
|
+
* AUTO_UPDATE_SOAK_MS in src/extensions.js). Off by default: the update
|
|
169
|
+
* badge is the prompt, the button is the decision. */
|
|
170
|
+
autoUpdateExtensions: false,
|
|
162
171
|
/** Show region-limited models in the picker as soon as they answer. */
|
|
163
172
|
exposeRegionModels: true,
|
|
164
173
|
/** Advertisement only: retain catalog entries and historical model resolution. */
|
|
@@ -167,8 +176,15 @@ export const SETTINGS_INITIAL = {
|
|
|
167
176
|
hideEacModels: false,
|
|
168
177
|
/** Minutes between background catalog refreshes. Automatic availability
|
|
169
178
|
* probing is disabled by owner policy; only the manual reprobe button sends
|
|
170
|
-
* probe requests.
|
|
179
|
+
* probe requests. Since 2.8.1 this cadence also cannot re-fetch the listing
|
|
180
|
+
* more often than once a listing window (see src/core/runtime.js). */
|
|
171
181
|
probeIntervalMinutes: 15,
|
|
182
|
+
/** The OpenCode lane's own proxy exit (issue #708's method): when set, this
|
|
183
|
+
* lane's requests dial through it instead of the process's global egress,
|
|
184
|
+
* which is how a burned per-IP quota window is left behind. Empty means the
|
|
185
|
+
* global outlet when one runs, then the HTTP(S)_PROXY environment, then
|
|
186
|
+
* direct. Never auto-rotated — the address is the user's own. */
|
|
187
|
+
freeLane: { proxyUrl: '' },
|
|
172
188
|
/** Serve the OpenAI-compatible forward listener for other local harnesses. */
|
|
173
189
|
forward: {
|
|
174
190
|
enabled: false,
|
package/src/stream.js
CHANGED
|
@@ -88,11 +88,13 @@ function argumentsCutMidway(raw) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
class BlockSink {
|
|
91
|
-
constructor(yieldChunk, startIndex, checkpointLimit, onProgress) {
|
|
91
|
+
constructor(yieldChunk, startIndex, checkpointLimit, onProgress, renameMap) {
|
|
92
92
|
this.emit = yieldChunk
|
|
93
93
|
this.next = startIndex
|
|
94
94
|
this.onProgress = typeof onProgress === 'function' ? onProgress : () => {}
|
|
95
|
-
/**
|
|
95
|
+
/** The rename map the raw Chat tool names resolve through. */
|
|
96
|
+
this.renameMap = renameMap
|
|
97
|
+
/** @type {Map<string, {index:number, kind:string, text:string, id?:string, name?:string, rawName?:string, args?:string}>} */
|
|
96
98
|
this.open = new Map()
|
|
97
99
|
this.usage = undefined
|
|
98
100
|
this.sawReasoning = false
|
|
@@ -186,10 +188,40 @@ class BlockSink {
|
|
|
186
188
|
&& (block.name !== priorName || block.id !== priorId)) this.onProgress()
|
|
187
189
|
}
|
|
188
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Buffer one fragment of a Chat tool call's wire name (`toolNameFragment`).
|
|
193
|
+
*
|
|
194
|
+
* Some gateways split `tool_calls[].function.name` across delta frames — and
|
|
195
|
+
* resolving every fragment on arrival used to fold a half name into a wrong
|
|
196
|
+
* or unknown tool, surfacing ghost calls (opencode2api v1.4.7 buffers the
|
|
197
|
+
* same way). The raw name accumulates here and resolves through
|
|
198
|
+
* {@link resolveRawName} once the call's arguments start or the block closes;
|
|
199
|
+
* until then the emitted deltas simply carry no name, which downstream
|
|
200
|
+
* consumers already tolerate (arguments can legitimately precede a name).
|
|
201
|
+
*/
|
|
202
|
+
toolNameFragment(key, id, fragment) {
|
|
203
|
+
const block = this.slot(key, 'tool-call')
|
|
204
|
+
if (id) block.id = id
|
|
205
|
+
// Real servers repeat the *complete* name on every delta; a genuinely split
|
|
206
|
+
// name arrives as disjoint fragments. Extend when the fragment continues
|
|
207
|
+
// (or repeats) what we hold, concatenate only when it cannot be a repeat —
|
|
208
|
+
// that covers both shapes without ever doubling a repeated name.
|
|
209
|
+
const held = typeof block.rawName === 'string' ? block.rawName : ''
|
|
210
|
+
block.rawName = fragment.startsWith(held) ? fragment : held + fragment
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Resolve a buffered raw wire name once, right before its first consumer needs it. */
|
|
214
|
+
#resolveRawName(block) {
|
|
215
|
+
if (block.name || typeof block.rawName !== 'string' || block.rawName === '') return
|
|
216
|
+
block.name = restoreToolName(block.rawName, this.renameMap)
|
|
217
|
+
this.onProgress()
|
|
218
|
+
}
|
|
219
|
+
|
|
189
220
|
toolArgs(key, delta) {
|
|
190
221
|
this.sawToolCall = true
|
|
191
222
|
if (!delta) return
|
|
192
223
|
const block = this.slot(key, 'tool-call')
|
|
224
|
+
this.#resolveRawName(block)
|
|
193
225
|
block.args += delta
|
|
194
226
|
// Arguments can arrive before the wire has named the call (a Claude-relay
|
|
195
227
|
// stream does exactly this). The harness re-reads every chunk through a
|
|
@@ -211,6 +243,7 @@ class BlockSink {
|
|
|
211
243
|
if (block.kind === 'text' && block.text !== '') this.emit({ type: 'block-end', index: block.index, block: { type: 'text', text: block.text } })
|
|
212
244
|
else if (block.kind === 'reasoning' && block.text !== '') this.emit({ type: 'block-end', index: block.index, block: { type: 'reasoning', text: block.text } })
|
|
213
245
|
else if (block.kind === 'tool-call') {
|
|
246
|
+
this.#resolveRawName(block)
|
|
214
247
|
// Arguments that never parse are an unexecutable call. The gateway
|
|
215
248
|
// reports finish "tool_calls" even when the output ceiling cut the JSON
|
|
216
249
|
// mid-string (verified live 2026-09-25), so the finish token alone
|
|
@@ -359,7 +392,9 @@ function feedChat(sink, payload, renameMap, onFinish) {
|
|
|
359
392
|
sink.sawToolCall = true
|
|
360
393
|
const key = sink.toolKey(call)
|
|
361
394
|
const name = call.function?.name
|
|
362
|
-
|
|
395
|
+
// The name is buffered, not resolved per fragment: split frames used to
|
|
396
|
+
// fold a half name into a ghost tool (see BlockSink.toolNameFragment).
|
|
397
|
+
if (typeof name === 'string' && name !== '') sink.toolNameFragment(key, call.id ?? '', name)
|
|
363
398
|
else if (call.id) sink.toolStart(key, call.id, '')
|
|
364
399
|
if (call.function?.arguments) sink.toolArgs(key, call.function.arguments)
|
|
365
400
|
}
|
|
@@ -506,7 +541,7 @@ export async function * readStream(lines, wire, renameMap, now = () => Date.now(
|
|
|
506
541
|
const outbox = []
|
|
507
542
|
const startIndex = Number.isSafeInteger(options?.startIndex) && options.startIndex >= 0 ? options.startIndex : 0
|
|
508
543
|
const checkpointLimit = Number.isSafeInteger(options?.checkpointLimit) && options.checkpointLimit >= 0 ? options.checkpointLimit : 131072
|
|
509
|
-
const sink = new BlockSink(chunk => outbox.push(chunk), startIndex, checkpointLimit, options?.onProgress)
|
|
544
|
+
const sink = new BlockSink(chunk => outbox.push(chunk), startIndex, checkpointLimit, options?.onProgress, renameMap)
|
|
510
545
|
const state = { usage: undefined, finish: undefined, sawFinish: false, sawUsage: false, sawToolCall: false, firstDeltaAt: undefined, sawReasoning: false, sawText: false, brokenToolCall: false }
|
|
511
546
|
const snapshot = () => ({
|
|
512
547
|
...state,
|
|
@@ -335,10 +335,11 @@ function credentialRedactor(secrets, deliver) {
|
|
|
335
335
|
* complete() may override it with request.resolveImage for that call only.
|
|
336
336
|
*/
|
|
337
337
|
export function createSubscriptionRuntime({
|
|
338
|
-
subscriptions, stats, logger, fetchImpl = globalThis.fetch, timeoutMs = 300000, resolveImage,
|
|
338
|
+
subscriptions, stats, logger, fetchImpl = globalThis.fetch, timeoutMs = 300000, resolveImage, onPayload,
|
|
339
339
|
} = {}) {
|
|
340
340
|
if (typeof subscriptions?.list !== 'function' || typeof subscriptions?.resolve !== 'function'
|
|
341
341
|
|| (resolveImage !== undefined && typeof resolveImage !== 'function')
|
|
342
|
+
|| (onPayload !== undefined && typeof onPayload !== 'function')
|
|
342
343
|
|| typeof fetchImpl !== 'function' || !Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 300000) {
|
|
343
344
|
throw failure('INVALID_INPUT', 'Invalid subscription runtime configuration', 400)
|
|
344
345
|
}
|
|
@@ -388,6 +389,14 @@ export function createSubscriptionRuntime({
|
|
|
388
389
|
onChunk(chunk)
|
|
389
390
|
foldForwardOutcome(outcome, chunk)
|
|
390
391
|
})
|
|
392
|
+
let body = payload
|
|
393
|
+
if (onPayload !== undefined) {
|
|
394
|
+
// The owner's body seam (enhancement extensions on the magpie lane):
|
|
395
|
+
// fail-open is the hook's own contract, but a broken seam itself must
|
|
396
|
+
// not take the turn down — ship the unmodified payload instead.
|
|
397
|
+
try { body = (await Promise.race([onPayload(payload, { provider: config.provider, model: config.model, wire: config.wire }), interrupted])) ?? payload }
|
|
398
|
+
catch (error) { if (controller.signal.aborted) throw error }
|
|
399
|
+
}
|
|
391
400
|
try {
|
|
392
401
|
response = await Promise.race([fetchImpl(config.endpoint, {
|
|
393
402
|
method: 'POST', redirect: 'error', signal: controller.signal,
|
|
@@ -395,7 +404,7 @@ export function createSubscriptionRuntime({
|
|
|
395
404
|
...config.headers, 'content-type': 'application/json', accept: 'text/event-stream',
|
|
396
405
|
...(config.provider === 'gemini' ? { 'user-agent': `GeminiCLI/0.61.0/${config.model} (${process.platform}; ${process.arch}; terminal)` } : {}),
|
|
397
406
|
},
|
|
398
|
-
body: JSON.stringify(
|
|
407
|
+
body: JSON.stringify(body),
|
|
399
408
|
}), interrupted])
|
|
400
409
|
} catch (error) {
|
|
401
410
|
if (controller.signal.aborted) throw error
|
package/src/updater.js
CHANGED
|
@@ -161,39 +161,11 @@ const restartRequiredVersions = globalThis[RESTART_MARKER] ??= new Map()
|
|
|
161
161
|
const UPGRADE_LOCKS = Symbol.for('our-free-model.upgrade-locks')
|
|
162
162
|
const upgradeLocks = globalThis[UPGRADE_LOCKS] ??= new Set()
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), pre }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** -1 | 0 | 1 in semver precedence; a pre-release sorts before its release. */
|
|
173
|
-
export function compareVersions(a, b) {
|
|
174
|
-
const left = parseVersion(a)
|
|
175
|
-
const right = parseVersion(b)
|
|
176
|
-
if (left === null || right === null) return a === b ? 0 : a > b ? 1 : -1
|
|
177
|
-
for (const key of ['major', 'minor', 'patch']) {
|
|
178
|
-
if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1
|
|
179
|
-
}
|
|
180
|
-
if (left.pre === null && right.pre === null) return 0
|
|
181
|
-
if (left.pre === null) return 1
|
|
182
|
-
if (right.pre === null) return -1
|
|
183
|
-
const width = Math.max(left.pre.length, right.pre.length)
|
|
184
|
-
for (let i = 0; i < width; i += 1) {
|
|
185
|
-
const x = left.pre[i]
|
|
186
|
-
const y = right.pre[i]
|
|
187
|
-
if (x === undefined) return -1
|
|
188
|
-
if (y === undefined) return 1
|
|
189
|
-
const xn = /^\d+$/.test(x)
|
|
190
|
-
const yn = /^\d+$/.test(y)
|
|
191
|
-
if (xn && yn && Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
|
|
192
|
-
if (xn !== yn) return xn ? -1 : 1
|
|
193
|
-
if (x !== y) return x < y ? -1 : 1
|
|
194
|
-
}
|
|
195
|
-
return 0
|
|
196
|
-
}
|
|
164
|
+
// Semver comparison lives in ./semver.js — the standalone package ships
|
|
165
|
+
// without the updater but with the extension platform, which compares a
|
|
166
|
+
// registry entry's minHost against the host version.
|
|
167
|
+
import { parseVersion, compareVersions } from './semver.js'
|
|
168
|
+
export { parseVersion, compareVersions }
|
|
197
169
|
|
|
198
170
|
/** Detect a plugin install whose package files still belong to a package-manager store. */
|
|
199
171
|
export function packageUsesHardLinks(pkgDir) {
|
package/src/upstream.js
CHANGED
|
@@ -422,6 +422,45 @@ export function ensureFreeLaneEnvironment(body, style, environment = {}) {
|
|
|
422
422
|
appendSystemContext(body, shape, `Here is some useful information about the environment you are running in:\n<env>\n Working directory: ${line(cwd)}\n Platform: ${line(platform)}\n</env>`)
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
/**
|
|
426
|
+
* The request shapes the free tier reads as a conversation-title generator
|
|
427
|
+
* (`title.IsTitleRequest`, opencode2api v1.4.7): no tools declared, one to
|
|
428
|
+
* three turns, no assistant/tool/function turn, and a title marker in the
|
|
429
|
+
* system or leading text.
|
|
430
|
+
*
|
|
431
|
+
* The reference stopped shaping these bodies in v1.4.7 with a measured reason:
|
|
432
|
+
* the free tier answers an agent-shaped tool list on a toolless title call
|
|
433
|
+
* with 403/429. This lane injects the full official prompt and 13-tool set on
|
|
434
|
+
* every body, so the harness's title round-trips used to ship exactly that
|
|
435
|
+
* shape whenever the local title answer was switched off. A title request now
|
|
436
|
+
* ships as the caller wrote it — no prompt injection, no tool append, no
|
|
437
|
+
* renames.
|
|
438
|
+
*/
|
|
439
|
+
const TITLE_MARKERS = /generate a title|write a title|create a title|come up with a title|title (?:for|of) (?:this|the) (?:conversation|chat|session|exchange|request)|conversation title|summarize (?:this|the) conversation/i
|
|
440
|
+
|
|
441
|
+
export function isTitleRequest(body, style) {
|
|
442
|
+
if (!body || typeof body !== 'object') return false
|
|
443
|
+
if ((Array.isArray(body.tools) && body.tools.length > 0) || (Array.isArray(body.functions) && body.functions.length > 0)) return false
|
|
444
|
+
if (body.tool_choice !== undefined) return false
|
|
445
|
+
const shape = shapeOf(style)
|
|
446
|
+
const textOf = value => typeof value === 'string' ? value
|
|
447
|
+
: Array.isArray(value) ? value.map(part => part?.text ?? '').join('\n') : ''
|
|
448
|
+
let turns
|
|
449
|
+
let leading = ''
|
|
450
|
+
if (shape === 'flat') {
|
|
451
|
+
turns = (Array.isArray(body.input) ? body.input : []).filter(item => item !== null && typeof item === 'object' && typeof item.role === 'string')
|
|
452
|
+
leading = textOf(body.instructions)
|
|
453
|
+
} else {
|
|
454
|
+
turns = (Array.isArray(body.messages) ? body.messages : []).filter(message => message !== null && typeof message === 'object')
|
|
455
|
+
leading = shape === 'claude' ? textOf(body.system) : ''
|
|
456
|
+
}
|
|
457
|
+
if (turns.length === 0 || turns.length > 3) return false
|
|
458
|
+
for (const turn of turns) {
|
|
459
|
+
if (!['user', 'system', 'developer'].includes(String(turn.role ?? ''))) return false
|
|
460
|
+
}
|
|
461
|
+
return TITLE_MARKERS.test(`${leading}\n${turns.map(turn => textOf(turn.content)).join('\n')}`)
|
|
462
|
+
}
|
|
463
|
+
|
|
425
464
|
/**
|
|
426
465
|
* Satisfy the v2 free-tier fingerprint on `body`, shaped as the reference
|
|
427
466
|
* gateway shapes it (opencode2api v1.4.6, see the module header):
|
|
@@ -452,6 +491,10 @@ export function applyFingerprint(body, style) {
|
|
|
452
491
|
const shape = shapeOf(style)
|
|
453
492
|
if (!body || typeof body !== 'object') return new ToolRenameMap()
|
|
454
493
|
|
|
494
|
+
// 0. Title requests ship unshaped: an agent-shaped tool list on a toolless
|
|
495
|
+
// title call is a 403/429 the reference measured (see isTitleRequest).
|
|
496
|
+
if (isTitleRequest(body, style)) return new ToolRenameMap()
|
|
497
|
+
|
|
455
498
|
// 1. Official system prompt (the reference injects before shaping tools).
|
|
456
499
|
injectOfficialPrompt(body, shape)
|
|
457
500
|
|
|
@@ -43,7 +43,7 @@ OFM_ESBUILD_DIR=/tmp/ofm-channel-build npm run channel-pack
|
|
|
43
43
|
- `src/index.ts`:增加可选 `disableOpencode` 挂载配置;OFM 开启它时跳过重复 OpenCode adapter、默认匿名账号和能力表预热,保留历史账号及凭据。省略时维持上游行为。
|
|
44
44
|
- `src/channel-pack-rpc.ts`:追加同一配置,关闭时拒绝 `opencode.*` 与通用 `account.create(provider=opencode)`。
|
|
45
45
|
- 打包入口改为 `src/index.ts`;宿主模块仍保持外部,undici 内联。
|
|
46
|
-
- 再构建使用 esbuild 0.25.12,undici 8.
|
|
46
|
+
- 再构建使用 esbuild 0.25.12,undici 8.12.2 使用根目录锁文件,与插件和独立端的运行产物保持一致。
|
|
47
47
|
- `src/credential-expiry.ts` 与 `src/credit-expiry-window.ts` 提供渠道间共享的纯函数。积分临时窗口默认 15 天,可通过 `DSH_CREDIT_EXPIRING_WINDOW_DAYS` 显式设置为非负天数。
|
|
48
48
|
|
|
49
49
|
本目录**不含**上游的浏览器半身(`plugin-src/client`)与其设置页——
|