@dan-ai-studio/dshopencodego 0.1.42 → 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 +43 -44
- 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
package/lib/types/go-limits.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The frozen seed of Go's published per-model quota.
|
|
3
|
-
*
|
|
4
|
-
* No API exposes these numbers — the gateway's `/v1/models` answers ids and
|
|
5
|
-
* `/usage` answers account-wide percentages — so the live figures are parsed
|
|
6
|
-
* from the provider's own documentation by `catalog/go-doc.ts`. This table is
|
|
7
|
-
* the seed and the last resort behind that fetch:
|
|
8
|
-
*
|
|
9
|
-
* - it renders instantly at startup, before the first fetch resolves;
|
|
10
|
-
* - it keeps working when every source fails, including the day the document
|
|
11
|
-
* URL moves.
|
|
12
|
-
*
|
|
13
|
-
* It is deliberately **not** updated as models arrive: wherever it is shown its
|
|
14
|
-
* transcription date travels with it, so a stale number is never silent, and a
|
|
15
|
-
* model it does not list simply shows no allowance until a fetch succeeds.
|
|
16
|
-
*
|
|
17
|
-
* Source: https://opencode.ai/docs/zh-cn/go — the 「使用限制」 and
|
|
18
|
-
* 「预估请求数」 tables. Transcribed 2026-09-25, frozen 2026-09-26.
|
|
19
|
-
*
|
|
20
|
-
* Two caveats are part of the data's meaning, not noise:
|
|
21
|
-
* - `monthlyUsd` is the hard monthly allowance; the request counts are the
|
|
22
|
-
* provider's own estimate for a typical request mix, not a hard ceiling.
|
|
23
|
-
* - DeepSeek V4.1 Flash carried a limited-time 4x allowance in this snapshot;
|
|
24
|
-
* the provider has since made the listed $60 permanent, which only the live
|
|
25
|
-
* document reports.
|
|
26
|
-
*
|
|
27
|
-
* @module @dan-ai-studio/dshopencodego/go-limits
|
|
28
|
-
*/
|
|
29
|
-
/** The date this seed was transcribed from the provider's page. */
|
|
30
|
-
export const SEED_TRANSCRIBED = '2026-09-25';
|
|
31
|
-
/** The frozen seed table, keyed by model id. Not a maintenance target. */
|
|
32
|
-
export const SEED_QUOTAS = {
|
|
33
|
-
'glm-5.3-flash': { monthlyUsd: 60, monthlyRequests: 31_580 },
|
|
34
|
-
'glm-5.3': { monthlyUsd: 15, monthlyRequests: 1_080 },
|
|
35
|
-
'glm-5.2': { monthlyUsd: 60, monthlyRequests: 4_300 },
|
|
36
|
-
'glm-5.1': { monthlyUsd: 60, monthlyRequests: 4_300 },
|
|
37
|
-
'kimi-k3': { monthlyUsd: 15, monthlyRequests: 490 },
|
|
38
|
-
'kimi-k2.7-code': { monthlyUsd: 60, monthlyRequests: 6_750 },
|
|
39
|
-
'kimi-k2.6': { monthlyUsd: 60, monthlyRequests: 5_750 },
|
|
40
|
-
'longcat-2.0': { monthlyUsd: 60, monthlyRequests: 57_200 },
|
|
41
|
-
'mimo-v2.6-flash': { monthlyUsd: 60, monthlyRequests: 150_400 },
|
|
42
|
-
'mimo-v2.6-pro': { monthlyUsd: 15, monthlyRequests: 16_300 },
|
|
43
|
-
'mimo-v2.5': { monthlyUsd: 60, monthlyRequests: 150_400 },
|
|
44
|
-
'mimo-v2.5-pro': { monthlyUsd: 15, monthlyRequests: 16_300 },
|
|
45
|
-
'minimax-m3': { monthlyUsd: 60, monthlyRequests: 16_000 },
|
|
46
|
-
'minimax-m2.7': { monthlyUsd: 60, monthlyRequests: 17_000 },
|
|
47
|
-
'minimax-m2.5': { monthlyUsd: 60 },
|
|
48
|
-
'muse-spark-1.3-contributor': { monthlyUsd: 60, monthlyRequests: 226_600 },
|
|
49
|
-
'muse-spark-1.2-contributor': { monthlyUsd: 60, monthlyRequests: 226_600 },
|
|
50
|
-
'qwen3.8-max': { monthlyUsd: 15, monthlyRequests: 810 },
|
|
51
|
-
'qwen3.8-flash': { monthlyUsd: 30, monthlyRequests: 27_000 },
|
|
52
|
-
'qwen3.7-max': { monthlyUsd: 30, monthlyRequests: 840 },
|
|
53
|
-
'qwen3.7-plus': { monthlyUsd: 60, monthlyRequests: 21_600 },
|
|
54
|
-
'qwen3.6-plus': { monthlyUsd: 60, monthlyRequests: 16_300 },
|
|
55
|
-
'deepseek-v4.1-flash': { monthlyUsd: 60, monthlyRequests: 130_000 },
|
|
56
|
-
'deepseek-v4-pro': { monthlyUsd: 15, monthlyRequests: 5_200 },
|
|
57
|
-
'deepseek-v4-flash': { monthlyUsd: 30, monthlyRequests: 65_000 },
|
|
58
|
-
'deepseek-v4-flash-vision-exp': { monthlyUsd: 15, monthlyRequests: 32_500 },
|
|
59
|
-
'hy4-preview': { monthlyUsd: 30, monthlyRequests: 6_770 },
|
|
60
|
-
'hy3': { monthlyUsd: 60, monthlyRequests: 21_500 },
|
|
61
|
-
// 注意:space-bunny-free 并不在 Go 端点支持的 37 个模型列表中(请求会返回 400 错误),已依据 R-B4 予以剔除
|
|
62
|
-
'grok-4.7': { monthlyUsd: 15, monthlyRequests: 845 },
|
|
63
|
-
'grok-4.6': { monthlyUsd: 15, monthlyRequests: 845 },
|
|
64
|
-
'gpt-6-luna': { monthlyUsd: 15, monthlyRequests: 21_130 },
|
|
65
|
-
'gpt-5.6-luna': { monthlyUsd: 15, monthlyRequests: 10_250 },
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* The seed allowance for one model id.
|
|
69
|
-
* @param id - the gateway's model id.
|
|
70
|
-
* @returns the frozen seed entry, or undefined when the page listed none.
|
|
71
|
-
*/
|
|
72
|
-
export function goQuotaFor(id) {
|
|
73
|
-
return Object.hasOwn(SEED_QUOTAS, id) ? SEED_QUOTAS[id] : undefined;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Rank a quota for "most usable first" ordering: unlimited beats any finite
|
|
77
|
-
* request estimate, a larger estimate beats a smaller one, and a model the
|
|
78
|
-
* page does not quantify sorts last.
|
|
79
|
-
* @param quota - the allowance under comparison, when one exists.
|
|
80
|
-
* @returns a sortable rank.
|
|
81
|
-
*/
|
|
82
|
-
export function monthlyRequestsRank(quota) {
|
|
83
|
-
if (quota === undefined)
|
|
84
|
-
return -1;
|
|
85
|
-
const requests = quota.monthlyRequests;
|
|
86
|
-
if (requests === 'unlimited')
|
|
87
|
-
return Number.MAX_SAFE_INTEGER;
|
|
88
|
-
if (requests === undefined)
|
|
89
|
-
return quota.monthlyUsd === 'unlimited' ? Number.MAX_SAFE_INTEGER : -1;
|
|
90
|
-
return requests;
|
|
91
|
-
}
|
package/lib/types/index.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `dshopencodego` plugin: one `opencode-go` route with a live catalog and
|
|
3
|
-
* the gateway's mandatory session header.
|
|
4
|
-
*
|
|
5
|
-
* The plugin exists because a generic pi-ai route cannot express two things the
|
|
6
|
-
* OpenCode Go gateway needs: a model list that rotates faster than any shipped
|
|
7
|
-
* catalog, and a per-conversation `x-opencode-session` routing header on every
|
|
8
|
-
* inference request.
|
|
9
|
-
*
|
|
10
|
-
* Route registration is gated on both configuration and credential: a route
|
|
11
|
-
* whose key is missing would otherwise sit in every model picker and read as a
|
|
12
|
-
* usable provider to first-run onboarding. The gate re-evaluates on every
|
|
13
|
-
* credential write and every loader update, and a route another adapter already
|
|
14
|
-
* owns is reported rather than crashing the mount.
|
|
15
|
-
*
|
|
16
|
-
* ```yaml
|
|
17
|
-
* - id: dshopencodego
|
|
18
|
-
* name: '@dan-ai-studio/dshopencodego'
|
|
19
|
-
* config:
|
|
20
|
-
* enabled: true # false withdraws the route only
|
|
21
|
-
* apiKeyEnv: OPENCODE_GO_API_KEY # default
|
|
22
|
-
* baseURL: https://opencode.ai/inference/go/v1 # default
|
|
23
|
-
* refreshMinutes: 60 # live catalog TTL
|
|
24
|
-
* modelProtocols: # last-resort protocol override
|
|
25
|
-
* some-model: openai-responses
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @module @dan-ai-studio/dshopencodego
|
|
29
|
-
*/
|
|
30
|
-
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
31
|
-
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
32
|
-
import * as llm from '@deepseek-ai/dsh-llm';
|
|
33
|
-
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
34
|
-
import { OpencodeGoAdapter } from './adapter.js';
|
|
35
|
-
import { DISPLAY_NAME, PROVIDER_ID, discoverCatalogModels } from './catalog/index.js';
|
|
36
|
-
import { assertBaseURL, PlainConfig, readConfig } from './config.js';
|
|
37
|
-
import { registerRemotes } from './remotes.js';
|
|
38
|
-
import { OpencodeGoCatalogService } from './catalog/service.js';
|
|
39
|
-
import { OpencodeGoUpdateService } from './update/index.js';
|
|
40
|
-
import { PLUGIN_VERSION } from './update/index.js';
|
|
41
|
-
import { OpencodeGoUsageService, UsageMeter } from './usage/index.js';
|
|
42
|
-
export { OpencodeGoAdapter } from './adapter.js';
|
|
43
|
-
export { DEFAULT_BASE_URL, CONSOLE_BASE_URL, DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog, discoverCatalogModels } from './catalog/index.js';
|
|
44
|
-
export { Config, PlainConfig, assertBaseURL, DEFAULT_API_KEY_ENV } from './config.js';
|
|
45
|
-
export { SESSION_HEADER, SESSION_AFFINITY_HEADER, opencodeSessionValue, providerHeaders } from './session-header.js';
|
|
46
|
-
export { isModelEnabled, sortModels } from './models.js';
|
|
47
|
-
export const name = 'dshopencodego';
|
|
48
|
-
export const inject = ['llm'];
|
|
49
|
-
/** True when a loader handed this plugin its live field references. */
|
|
50
|
-
function isLiveConfig(raw) {
|
|
51
|
-
return typeof raw === 'object' && raw !== null && typeof raw.enabled === 'object'
|
|
52
|
-
&& typeof (raw.enabled?.get) === 'function';
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Register the route, its discovery, and their teardown for one mount.
|
|
56
|
-
*
|
|
57
|
-
* Configuration is read through a live source so a profile edit reaches the
|
|
58
|
-
* next request without a restart; the adapter re-reads it at every operation.
|
|
59
|
-
* @param ctx - the plugin's Cordis context.
|
|
60
|
-
* @param raw - the loader's live config, or a plain object in tests.
|
|
61
|
-
*/
|
|
62
|
-
export function apply(ctx, raw) {
|
|
63
|
-
const live = isLiveConfig(raw) ? raw : undefined;
|
|
64
|
-
const constant = live === undefined ? PlainConfig((raw ?? {})) : undefined;
|
|
65
|
-
const current = () => (live === undefined ? constant : readConfig(live));
|
|
66
|
-
assertBaseURL(current().baseURL);
|
|
67
|
-
/** Resolve the route credential; a named reference that misses fails loud. */
|
|
68
|
-
const resolveApiKey = async () => {
|
|
69
|
-
const ref = current().apiKeyEnv;
|
|
70
|
-
const credentials = ctx.get('credentials');
|
|
71
|
-
const hit = credentials !== undefined
|
|
72
|
-
? (await credentials.resolve(credentialRef(ref)))?.value
|
|
73
|
-
// Without the credentials seam the process environment is the whole
|
|
74
|
-
// credential plane.
|
|
75
|
-
: launchEnvironmentOf(ctx).get(ref)?.value;
|
|
76
|
-
if (hit !== undefined && hit.length > 0)
|
|
77
|
-
return llm.assertUsableApiKey(hit, name, ref);
|
|
78
|
-
throw new LlmError(`dshopencodego: no credential; the profile resolves ${ref}, which is not set — store ${ref} through the`
|
|
79
|
-
+ ' credentials service (the Web Models page writes it) or export it', 'MISSING_CREDENTIAL');
|
|
80
|
-
};
|
|
81
|
-
const meter = new UsageMeter();
|
|
82
|
-
const adapter = new OpencodeGoAdapter({
|
|
83
|
-
config: current,
|
|
84
|
-
resolveApiKey,
|
|
85
|
-
imageAccess: {
|
|
86
|
-
resolveAttachments: () => ctx.get('attachments'),
|
|
87
|
-
resolveImageAccess: (attachments, ref) => llm.resolveImageAttachmentAccess(attachments, hostPath => ctx.get('fs')?.processPathFromHostPath(hostPath), ref),
|
|
88
|
-
},
|
|
89
|
-
onFallback: ({ url, error }) => {
|
|
90
|
-
ctx.logger.warn(`dshopencodego: could not refresh ${url}; using the last known model data (${String(error)})`);
|
|
91
|
-
},
|
|
92
|
-
onUnconfigured: (entries) => {
|
|
93
|
-
ctx.logger.warn(`dshopencodego: gateway models this build cannot configure: ${entries.map(entry => `${entry.id} (${entry.reason})`).join(', ')}`);
|
|
94
|
-
},
|
|
95
|
-
onReplayDegrade: (reason) => {
|
|
96
|
-
ctx.logger.warn(`dshopencodego: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
97
|
-
},
|
|
98
|
-
onUsage: ({ model, usage }) => { meter.record(model, usage); },
|
|
99
|
-
});
|
|
100
|
-
// The usage Remote is mounted before the route so a picker can read it even
|
|
101
|
-
// while the route is withdrawn for a missing credential.
|
|
102
|
-
registerRemotes(ctx);
|
|
103
|
-
ctx.plugin(OpencodeGoUsageService, {
|
|
104
|
-
baseURL: () => current().baseURL,
|
|
105
|
-
resolveApiKey,
|
|
106
|
-
meter,
|
|
107
|
-
});
|
|
108
|
-
ctx.plugin(OpencodeGoCatalogService, {
|
|
109
|
-
catalog: () => adapter.catalogOf(current()),
|
|
110
|
-
visibility: () => current().modelVisibility,
|
|
111
|
-
});
|
|
112
|
-
ctx.plugin(OpencodeGoUpdateService, { currentVersion: PLUGIN_VERSION });
|
|
113
|
-
let registration;
|
|
114
|
-
let visibilityFacts = JSON.stringify(current().modelVisibility);
|
|
115
|
-
/**
|
|
116
|
-
* Register the route while it is enabled and its credential resolves, and
|
|
117
|
-
* drop it when either says no.
|
|
118
|
-
*
|
|
119
|
-
* Nothing else is torn down with the route: model discovery and the
|
|
120
|
-
* configuration surface stay mounted, so the switch that withdrew the route
|
|
121
|
-
* stays reachable to bring it back.
|
|
122
|
-
*/
|
|
123
|
-
const applyRoute = (credentialConfigured) => {
|
|
124
|
-
const enabled = current().enabled;
|
|
125
|
-
if (enabled && credentialConfigured && registration === undefined) {
|
|
126
|
-
try {
|
|
127
|
-
registration = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
// Most likely DUPLICATE_ADAPTER: another adapter family already owns
|
|
131
|
-
// `opencode-go` (an `llm-pi-ai` profile, or the previous plugin).
|
|
132
|
-
// Everything else this mount does keeps working.
|
|
133
|
-
ctx.logger.error(`dshopencodego: not registering the "${PROVIDER_ID}" route — another adapter already owns it;`
|
|
134
|
-
+ ` uninstall or disable the other provider first (${String(error)})`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else if ((!enabled || !credentialConfigured) && registration !== undefined) {
|
|
138
|
-
registration();
|
|
139
|
-
registration = undefined;
|
|
140
|
-
if (!enabled) {
|
|
141
|
-
ctx.logger.info('dshopencodego: disabled by configuration; the route and its models are withdrawn');
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
/** Re-evaluate the route gate against the credential that is in force now. */
|
|
146
|
-
const syncRoute = () => {
|
|
147
|
-
const visibility = JSON.stringify(current().modelVisibility);
|
|
148
|
-
if (visibility !== visibilityFacts) {
|
|
149
|
-
visibilityFacts = visibility;
|
|
150
|
-
// Replacing the owned route notifies every open picker without a restart.
|
|
151
|
-
registration?.replace([PROVIDER_ID]);
|
|
152
|
-
}
|
|
153
|
-
const credentials = ctx.get('credentials');
|
|
154
|
-
const ref = current().apiKeyEnv;
|
|
155
|
-
if (credentials === undefined) {
|
|
156
|
-
applyRoute(launchEnvironmentOf(ctx).get(ref)?.value !== undefined);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
void credentials.describe(credentialRef(ref))
|
|
160
|
-
.then((info) => {
|
|
161
|
-
// An answer applies only while it still answers for the reference in force.
|
|
162
|
-
if (ref === current().apiKeyEnv)
|
|
163
|
-
applyRoute(info.configured);
|
|
164
|
-
})
|
|
165
|
-
.catch((error) => {
|
|
166
|
-
ctx.logger.error(`dshopencodego: credential describe failed; keeping the previous route state (${String(error)})`);
|
|
167
|
-
});
|
|
168
|
-
};
|
|
169
|
-
syncRoute();
|
|
170
|
-
const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
|
|
171
|
-
if (request.provider !== PROVIDER_ID && !(request.baseURL ?? '').includes('opencode.ai')) {
|
|
172
|
-
throw new LlmError('dshopencodego discovers only OpenCode Go/Console endpoints; enter this provider\'s models by hand', 'DISCOVERY_UNSUPPORTED');
|
|
173
|
-
}
|
|
174
|
-
return discoverCatalogModels(adapter.catalogOf(current()));
|
|
175
|
-
});
|
|
176
|
-
ctx.effect(() => () => {
|
|
177
|
-
registration?.();
|
|
178
|
-
undiscover();
|
|
179
|
-
});
|
|
180
|
-
// Validate a profile edit before it is persisted, then re-evaluate the gate.
|
|
181
|
-
ctx.on('internal/config', function (_raw, next) {
|
|
182
|
-
const value = next();
|
|
183
|
-
if (this === ctx.fiber)
|
|
184
|
-
assertBaseURL(PlainConfig(value).baseURL);
|
|
185
|
-
return value;
|
|
186
|
-
});
|
|
187
|
-
ctx.on('loader/volatile-update', syncRoute);
|
|
188
|
-
ctx.inject(['credentials'], (credentialsCtx) => {
|
|
189
|
-
credentialsCtx.on('credentials/reference-updated', (ref) => {
|
|
190
|
-
if (ref === current().apiKeyEnv)
|
|
191
|
-
syncRoute();
|
|
192
|
-
});
|
|
193
|
-
// The seam can become visible after this plugin applied, so the boot-time
|
|
194
|
-
// call may have fallen back to the environment: sync again here.
|
|
195
|
-
syncRoute();
|
|
196
|
-
});
|
|
197
|
-
ctx.logger.info(`dshopencodego: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
|
|
198
|
-
}
|
package/lib/types/models.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The settings-facing model summary and its visibility rules.
|
|
3
|
-
*
|
|
4
|
-
* The adapter's `listModels` and the settings page both need the same facts and
|
|
5
|
-
* the same answer to "is this model offered right now", so the rule lives here
|
|
6
|
-
* once: a model nobody can configure is never offered, an explicit switch
|
|
7
|
-
* always wins, and a deprecated model defaults to off.
|
|
8
|
-
*
|
|
9
|
-
* @module @dan-ai-studio/dshopencodego/models
|
|
10
|
-
*/
|
|
11
|
-
import { goQuotaFor, monthlyRequestsRank } from './go-limits.js';
|
|
12
|
-
/** How many models the default configuration keeps enabled. */
|
|
13
|
-
export const DEFAULT_ENABLED_COUNT = 5;
|
|
14
|
-
/**
|
|
15
|
-
* Whether the model trades training rights for its price — Meta's
|
|
16
|
-
* "Contributor" tiers, whose prompts and completions may train future models.
|
|
17
|
-
* Such a model is never part of the default-enabled few.
|
|
18
|
-
* @param model - the model under test.
|
|
19
|
-
* @returns true when the model belongs to a training-contributor tier.
|
|
20
|
-
*/
|
|
21
|
-
export function isTrainingTier(model) {
|
|
22
|
-
return /contributor/i.test(model.id) || /contributor/i.test(model.name);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* The default-enabled ids when nobody configured switches: the models with the
|
|
26
|
-
* largest published monthly request estimate first, which is Go's own "most
|
|
27
|
-
* usable" order. Deprecated, unconfigurable, and training-contributor models
|
|
28
|
-
* never qualify, and a model with no published estimate does not displace one
|
|
29
|
-
* that has it.
|
|
30
|
-
*
|
|
31
|
-
* The estimate is looked up here, by id, rather than read from a `goQuota`
|
|
32
|
-
* field: the Host's picker and the settings page build their model lists from
|
|
33
|
-
* different projections, and a caller that forgot to carry the field would
|
|
34
|
-
* silently fall back to an alphabetical default — which is exactly the
|
|
35
|
-
* two-surfaces-disagree defect this signature exists to prevent.
|
|
36
|
-
* @param models - the advertised models, in any order.
|
|
37
|
-
* @returns the ids the default configuration keeps enabled.
|
|
38
|
-
*/
|
|
39
|
-
export function recommendedIds(models) {
|
|
40
|
-
const ranked = models
|
|
41
|
-
.filter(model => model.configurationMissing === undefined
|
|
42
|
-
&& model.deprecated !== true
|
|
43
|
-
&& !isTrainingTier(model))
|
|
44
|
-
.toSorted((left, right) => monthlyRequestsRank(goQuotaFor(right.id)) - monthlyRequestsRank(goQuotaFor(left.id))
|
|
45
|
-
|| left.id.localeCompare(right.id));
|
|
46
|
-
return new Set(ranked.slice(0, DEFAULT_ENABLED_COUNT).map(model => model.id));
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Whether the picker offers one model.
|
|
50
|
-
* @param model - the summary under test.
|
|
51
|
-
* @param visibility - per-model switches; an absent entry keeps the default.
|
|
52
|
-
* @returns true when the model should be selectable.
|
|
53
|
-
*/
|
|
54
|
-
export function isModelEnabled(model, visibility) {
|
|
55
|
-
if (model.configurationMissing !== undefined)
|
|
56
|
-
return false;
|
|
57
|
-
const explicit = visibility !== undefined && Object.hasOwn(visibility, model.id)
|
|
58
|
-
? visibility[model.id]
|
|
59
|
-
: undefined;
|
|
60
|
-
if (typeof explicit === 'boolean')
|
|
61
|
-
return explicit;
|
|
62
|
-
// A marked catalog decides the default; an unmarked one keeps every
|
|
63
|
-
// non-deprecated model, so callers that pass partial facts never lose models.
|
|
64
|
-
return typeof model.recommended === 'boolean' ? model.recommended : model.deprecated !== true;
|
|
65
|
-
}
|
|
66
|
-
/** A calendar date as models.dev states it, without a timezone. */
|
|
67
|
-
export function validReleaseDate(value) {
|
|
68
|
-
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
69
|
-
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
70
|
-
}
|
|
71
|
-
/** Whether a model shipped within the last week. */
|
|
72
|
-
function isNewModel(model, now = Date.now()) {
|
|
73
|
-
if (model.deprecated === true || !validReleaseDate(model.releaseDate))
|
|
74
|
-
return false;
|
|
75
|
-
const days = Math.floor(now / 86_400_000) - Date.parse(model.releaseDate) / 86_400_000;
|
|
76
|
-
return days >= 0 && days < 7;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Order models for display: newly shipped first, then the rest, then deprecated.
|
|
80
|
-
* Newest releases come first within the first group.
|
|
81
|
-
*/
|
|
82
|
-
export function sortModels(models, now = Date.now()) {
|
|
83
|
-
const rank = (model) => model.deprecated === true ? 2 : isNewModel(model, now) ? 0 : 1;
|
|
84
|
-
return [...models].sort((left, right) => rank(left) - rank(right)
|
|
85
|
-
|| (isNewModel(left, now) && isNewModel(right, now) ? right.releaseDate.localeCompare(left.releaseDate) : 0));
|
|
86
|
-
}
|
package/lib/types/remotes.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote registration.
|
|
3
|
-
*
|
|
4
|
-
* One registry owns one contribution per package, so both usage methods are
|
|
5
|
-
* mounted together after whichever registry activation order the composition
|
|
6
|
-
* happens to use.
|
|
7
|
-
*
|
|
8
|
-
* @module @dan-ai-studio/dshopencodego/remotes
|
|
9
|
-
*/
|
|
10
|
-
import { catalogRemote } from './catalog/contract.js';
|
|
11
|
-
import { updateRemote } from './update/contract.js';
|
|
12
|
-
import { usageRemote } from './usage/contract.js';
|
|
13
|
-
/** Every Remote method this package owns, mounted as one contribution. */
|
|
14
|
-
const contribution = {
|
|
15
|
-
package: usageRemote.package,
|
|
16
|
-
descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors, ...updateRemote.descriptors],
|
|
17
|
-
};
|
|
18
|
-
/** Register this package's Remote contribution for the mount's lifetime. */
|
|
19
|
-
export function registerRemotes(ctx) {
|
|
20
|
-
ctx.inject(['typert'], (scope) => {
|
|
21
|
-
scope.effect(() => scope.typert.register({
|
|
22
|
-
package: contribution.package,
|
|
23
|
-
face: 'host',
|
|
24
|
-
schemas: [],
|
|
25
|
-
model: { services: [], events: [], objects: [] },
|
|
26
|
-
invocations: contribution.descriptors,
|
|
27
|
-
}));
|
|
28
|
-
});
|
|
29
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The session header the OpenCode Go gateway requires.
|
|
3
|
-
*
|
|
4
|
-
* Every inference request must carry `x-opencode-session`; the gateway answers
|
|
5
|
-
* `400 MissingSessionID` without it and routes plus caches by its value. Two
|
|
6
|
-
* rules make that useful rather than merely accepted:
|
|
7
|
-
*
|
|
8
|
-
* 1. The value is the conversation's stable Harness session id, verbatim — the
|
|
9
|
-
* same string the session log records — so one conversation keeps one
|
|
10
|
-
* routing bucket and one prompt cache across turns, resume, compaction,
|
|
11
|
-
* retries, and subagents.
|
|
12
|
-
* 2. A request that carries no session id gets a fresh random value rather than
|
|
13
|
-
* a shared constant, because a constant would merge unrelated traffic into a
|
|
14
|
-
* single cache bucket and evict itself.
|
|
15
|
-
*
|
|
16
|
-
* @module @dan-ai-studio/dshopencodego/session-header
|
|
17
|
-
*/
|
|
18
|
-
import { randomUUID } from 'node:crypto';
|
|
19
|
-
import { attributionHeaders } from '@deepseek-ai/dsh-llm';
|
|
20
|
-
/** Header name the gateway requires. */
|
|
21
|
-
export const SESSION_HEADER = 'x-opencode-session';
|
|
22
|
-
/** Session affinity header for gateway routing and prompt caching. */
|
|
23
|
-
export const SESSION_AFFINITY_HEADER = 'x-session-affinity';
|
|
24
|
-
/**
|
|
25
|
-
* The header value for one request.
|
|
26
|
-
* @param sessionId - the request's session id, if it names one.
|
|
27
|
-
* @returns the exact session id, or a fresh UUID when there is none.
|
|
28
|
-
*/
|
|
29
|
-
export function opencodeSessionValue(sessionId) {
|
|
30
|
-
return sessionId !== undefined && sessionId.length > 0 ? sessionId : randomUUID();
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Every header one provider request carries.
|
|
34
|
-
*
|
|
35
|
-
* The attribution `user-agent` is Harness-owned and always present; the session
|
|
36
|
-
* and affinity headers are added on top of it. A caller-supplied header of the
|
|
37
|
-
* same name can never win, because the gateway's routing decision must follow
|
|
38
|
-
* the session, not a deployment string.
|
|
39
|
-
* @param sessionId - the request's session id, if it names one.
|
|
40
|
-
* @returns headers for the pi-ai request options.
|
|
41
|
-
*/
|
|
42
|
-
export function providerHeaders(sessionId) {
|
|
43
|
-
const session = opencodeSessionValue(sessionId);
|
|
44
|
-
return {
|
|
45
|
-
[SESSION_HEADER]: session,
|
|
46
|
-
[SESSION_AFFINITY_HEADER]: session,
|
|
47
|
-
...attributionHeaders(),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Self update check: is a newer release of this package published?
|
|
3
|
-
*
|
|
4
|
-
* This module only *reads*: it asks the npm registry for the latest published
|
|
5
|
-
* version and compares it with the running build. It never downloads,
|
|
6
|
-
* installs, or replaces anything — the actual update stays with the host's
|
|
7
|
-
* `pluginManager` (`removeBundle` + `installBundle`), driven by an explicit
|
|
8
|
-
* user click on the settings page. Automatic self-replacement would bypass
|
|
9
|
-
* the host's manifest lock, peer-compatibility checks, build approvals, and
|
|
10
|
-
* HMR lifecycle, so it is deliberately not implemented here.
|
|
11
|
-
*
|
|
12
|
-
* The check runs only when the settings page asks (page open or manual
|
|
13
|
-
* refresh), never on the model-request path. An unreachable registry is
|
|
14
|
-
* reported as exactly that: `UpdateStatus.checked` stays false so the page
|
|
15
|
-
* says the lookup failed instead of presenting it as "already up to date".
|
|
16
|
-
*
|
|
17
|
-
* @module @dan-ai-studio/dshopencodego/update/check
|
|
18
|
-
*/
|
|
19
|
-
/** This package as addressed on the registry. */
|
|
20
|
-
export const PACKAGE_NAME = '@dan-ai-studio/dshopencodego';
|
|
21
|
-
/** The GitHub repository, used to link a release's own notes. */
|
|
22
|
-
export const REPOSITORY = 'dan-ai-studio/dshopencodego';
|
|
23
|
-
/** Document address answering `{ "version": "x.y.z", ... }`. */
|
|
24
|
-
export const REGISTRY_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
25
|
-
/** How long one lookup answers for before the registry is asked again. */
|
|
26
|
-
export const CHECK_CACHE_MS = 6 * 60 * 60 * 1000;
|
|
27
|
-
/** Bound on one registry round trip; expiry fails silent, never throws. */
|
|
28
|
-
export const CHECK_TIMEOUT_MS = 10_000;
|
|
29
|
-
/** Split `1.2.3-tag.4+build` into comparable parts; invalid input yields undefined. */
|
|
30
|
-
export function parseVersion(value) {
|
|
31
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+[0-9a-zA-Z.-]+)?$/.exec(value.trim());
|
|
32
|
-
if (match === undefined || match === null)
|
|
33
|
-
return undefined;
|
|
34
|
-
const [, major, minor, patch, prerelease] = match;
|
|
35
|
-
return {
|
|
36
|
-
core: [Number(major), Number(minor), Number(patch)],
|
|
37
|
-
prerelease: prerelease === undefined ? undefined : prerelease.split('.'),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Compare two version strings.
|
|
42
|
-
* @returns negative when `left` is older, positive when newer, zero when equal.
|
|
43
|
-
* @throws {Error} when either side is not a version.
|
|
44
|
-
*/
|
|
45
|
-
export function compareVersions(left, right) {
|
|
46
|
-
const ours = parseVersion(left);
|
|
47
|
-
const theirs = parseVersion(right);
|
|
48
|
-
if (ours === undefined || theirs === undefined)
|
|
49
|
-
throw new Error(`dshopencodego: cannot compare versions "${left}" and "${right}"`);
|
|
50
|
-
for (let index = 0; index < 3; index += 1) {
|
|
51
|
-
const difference = (ours.core[index] ?? 0) - (theirs.core[index] ?? 0);
|
|
52
|
-
if (difference !== 0)
|
|
53
|
-
return difference;
|
|
54
|
-
}
|
|
55
|
-
if (ours.prerelease === undefined && theirs.prerelease === undefined)
|
|
56
|
-
return 0;
|
|
57
|
-
if (ours.prerelease === undefined)
|
|
58
|
-
return 1;
|
|
59
|
-
if (theirs.prerelease === undefined)
|
|
60
|
-
return -1;
|
|
61
|
-
const width = Math.max(ours.prerelease.length, theirs.prerelease.length);
|
|
62
|
-
for (let index = 0; index < width; index += 1) {
|
|
63
|
-
const oursPart = ours.prerelease[index];
|
|
64
|
-
const theirsPart = theirs.prerelease[index];
|
|
65
|
-
if (oursPart === undefined)
|
|
66
|
-
return -1;
|
|
67
|
-
if (theirsPart === undefined)
|
|
68
|
-
return 1;
|
|
69
|
-
if (oursPart !== theirsPart)
|
|
70
|
-
return oursPart < theirsPart ? -1 : 1;
|
|
71
|
-
}
|
|
72
|
-
return 0;
|
|
73
|
-
}
|
|
74
|
-
/** True when `latest` is a newer release than `current`. Unparsable input is never an update. */
|
|
75
|
-
export function isUpdateAvailable(current, latest) {
|
|
76
|
-
if (latest === undefined)
|
|
77
|
-
return false;
|
|
78
|
-
try {
|
|
79
|
-
return compareVersions(latest, current) > 0;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/** Read the `version` field out of a registry `latest` document. */
|
|
86
|
-
export function parseLatestVersion(body) {
|
|
87
|
-
if (body === null || typeof body !== 'object')
|
|
88
|
-
return undefined;
|
|
89
|
-
const version = body['version'];
|
|
90
|
-
return typeof version === 'string' && parseVersion(version) !== undefined ? version : undefined;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Ask the registry for the latest published version, cached per process.
|
|
94
|
-
*
|
|
95
|
-
* Concurrent callers share one in-flight lookup. A lookup that fails for any
|
|
96
|
-
* reason — network, timeout, malformed document — resolves as "no update
|
|
97
|
-
* known" rather than rejecting, so the settings page never breaks because
|
|
98
|
-
* the registry is unreachable.
|
|
99
|
-
*/
|
|
100
|
-
export function createUpdateChecker(deps) {
|
|
101
|
-
const now = deps.now ?? Date.now;
|
|
102
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
103
|
-
let cached;
|
|
104
|
-
let inflight;
|
|
105
|
-
async function lookup() {
|
|
106
|
-
const checkedAt = now();
|
|
107
|
-
try {
|
|
108
|
-
const response = await fetchImpl(REGISTRY_LATEST_URL, { signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) });
|
|
109
|
-
if (!response.ok)
|
|
110
|
-
throw new Error(`registry answered ${response.status}`);
|
|
111
|
-
const latest = parseLatestVersion(await response.json());
|
|
112
|
-
return {
|
|
113
|
-
current: deps.currentVersion,
|
|
114
|
-
latest,
|
|
115
|
-
available: isUpdateAvailable(deps.currentVersion, latest),
|
|
116
|
-
checkedAt,
|
|
117
|
-
checked: true,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// The lookup failed, so no version is known. `checked: false` keeps that
|
|
122
|
-
// distinguishable from a confirmed "already latest" reading.
|
|
123
|
-
return { current: deps.currentVersion, latest: undefined, available: false, checkedAt, checked: false };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
check(force = false) {
|
|
128
|
-
if (!force && cached !== undefined && now() - cached.checkedAt < CHECK_CACHE_MS)
|
|
129
|
-
return Promise.resolve(cached);
|
|
130
|
-
inflight ??= lookup().then((status) => {
|
|
131
|
-
cached = status;
|
|
132
|
-
inflight = undefined;
|
|
133
|
-
return status;
|
|
134
|
-
});
|
|
135
|
-
return inflight;
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire contract for the update Remote.
|
|
3
|
-
*
|
|
4
|
-
* `readUpdate` answers whether a newer release of this package is published.
|
|
5
|
-
* The reading is advisory — applying the update stays a user click that
|
|
6
|
-
* drives the host's `pluginManager`, never an automatic download.
|
|
7
|
-
*
|
|
8
|
-
* @module @dan-ai-studio/dshopencodego/update/contract
|
|
9
|
-
*/
|
|
10
|
-
/** Validate an update reading crossing the wire. */
|
|
11
|
-
export function parseUpdateStatus(value) {
|
|
12
|
-
if (value === null || typeof value !== 'object')
|
|
13
|
-
throw new Error('invalid update reading');
|
|
14
|
-
const row = value;
|
|
15
|
-
if (typeof row['current'] !== 'string')
|
|
16
|
-
throw new Error('invalid update reading');
|
|
17
|
-
if (row['latest'] !== undefined && typeof row['latest'] !== 'string')
|
|
18
|
-
throw new Error('invalid update reading');
|
|
19
|
-
if (typeof row['available'] !== 'boolean')
|
|
20
|
-
throw new Error('invalid update reading');
|
|
21
|
-
if (typeof row['checked'] !== 'boolean')
|
|
22
|
-
throw new Error('invalid update reading');
|
|
23
|
-
if (typeof row['checkedAt'] !== 'number' || !Number.isFinite(row['checkedAt'])) {
|
|
24
|
-
throw new Error('invalid update reading');
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
current: row['current'],
|
|
28
|
-
latest: typeof row['latest'] === 'string' ? row['latest'] : undefined,
|
|
29
|
-
available: row['available'],
|
|
30
|
-
checkedAt: row['checkedAt'],
|
|
31
|
-
checked: row['checked'],
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/** Codec shape accepted by both released and source DSH builds. */
|
|
35
|
-
const codec = (typeSymbol, parse) => ({
|
|
36
|
-
mode: 'strict',
|
|
37
|
-
typeSymbol,
|
|
38
|
-
schema: { parse },
|
|
39
|
-
create: () => ({ parse }),
|
|
40
|
-
});
|
|
41
|
-
/** Remote methods this package owns, mounted together by the plugin. */
|
|
42
|
-
export const updateRemote = {
|
|
43
|
-
package: '@dan-ai-studio/dshopencodego',
|
|
44
|
-
descriptors: [
|
|
45
|
-
{
|
|
46
|
-
id: '@dan-ai-studio/dshopencodego#opencodeGoUpdate/readUpdate',
|
|
47
|
-
service: 'opencodeGoUpdate',
|
|
48
|
-
namespace: 'opencodeGoUpdate',
|
|
49
|
-
method: 'readUpdate',
|
|
50
|
-
invocation: { kind: 'direct' },
|
|
51
|
-
parameters: [],
|
|
52
|
-
result: codec('@dan-ai-studio/dshopencodego#UpdateStatus', parseUpdateStatus),
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: '@dan-ai-studio/dshopencodego#opencodeGoUpdate/refreshUpdate',
|
|
56
|
-
service: 'opencodeGoUpdate',
|
|
57
|
-
namespace: 'opencodeGoUpdate',
|
|
58
|
-
method: 'refreshUpdate',
|
|
59
|
-
invocation: { kind: 'direct' },
|
|
60
|
-
parameters: [],
|
|
61
|
-
result: codec('@dan-ai-studio/dshopencodego#UpdateStatus', parseUpdateStatus),
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Self update check (advisory only).
|
|
3
|
-
*
|
|
4
|
-
* @module @dan-ai-studio/dshopencodego/update
|
|
5
|
-
*/
|
|
6
|
-
export { PACKAGE_NAME, REGISTRY_LATEST_URL, CHECK_CACHE_MS, CHECK_TIMEOUT_MS } from './check.js';
|
|
7
|
-
export { compareVersions, createUpdateChecker, isUpdateAvailable, parseLatestVersion, parseVersion } from './check.js';
|
|
8
|
-
export { PLUGIN_VERSION } from './version.js';
|
|
9
|
-
export { OpencodeGoUpdateService } from './service.js';
|
|
10
|
-
export { parseUpdateStatus, updateRemote } from './contract.js';
|