dsh-openai-subscription 0.1.0 → 0.1.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 +44 -15
- package/README.zh.md +45 -16
- package/dist/client.d.ts +40 -29
- package/dist/client.js +565 -167
- package/dist/host.d.ts +23 -13
- package/dist/host.js +331 -72
- package/dist/models.d.ts +51 -0
- package/dist/models.js +370 -0
- package/package.json +9 -6
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Dynamic ChatGPT Codex model discovery and catalog ownership helpers. */
|
|
2
|
+
export type ModelInput = 'text' | 'image';
|
|
3
|
+
export type ReasoningEffort = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
4
|
+
/** The subset of an llm-pi-ai model profile this plugin discovers and owns. */
|
|
5
|
+
export interface ModelProfile {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
contextWindow?: number;
|
|
9
|
+
input?: ModelInput[];
|
|
10
|
+
reasoningEfforts?: false | Partial<Record<ReasoningEffort, string | null>>;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/** Credential fields needed by the authenticated model-list endpoint. */
|
|
14
|
+
export interface ModelDiscoveryCredential {
|
|
15
|
+
access: string;
|
|
16
|
+
accountId?: string;
|
|
17
|
+
}
|
|
18
|
+
export type ModelFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
19
|
+
export interface ModelDiscoveryOptions {
|
|
20
|
+
fetch?: ModelFetch;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DiscoveredModelCatalog {
|
|
25
|
+
/** Models the account-scoped picker exposes. */
|
|
26
|
+
models: ModelProfile[];
|
|
27
|
+
/** Every valid slug mentioned upstream, including explicitly hidden rows. */
|
|
28
|
+
seenIds: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface MergedModelCatalog {
|
|
31
|
+
/** Complete explicit settings list: live remote entries plus local custom entries. */
|
|
32
|
+
models: ModelProfile[];
|
|
33
|
+
/** Exact provider snapshot used as the base of the next three-way merge. */
|
|
34
|
+
managed: ModelProfile[];
|
|
35
|
+
/** Provider model ids a user removed explicitly and which must stay removed. */
|
|
36
|
+
suppressed: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert the authenticated Codex model response into llm-pi-ai model profiles.
|
|
40
|
+
* Only picker-visible models are adopted; malformed and duplicate rows are ignored.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseOpenAIModelCatalog(value: unknown): DiscoveredModelCatalog;
|
|
43
|
+
/** Fetch the live model catalog available to this exact ChatGPT account. */
|
|
44
|
+
export declare function discoverOpenAIModels(credential: ModelDiscoveryCredential, options?: ModelDiscoveryOptions): Promise<DiscoveredModelCatalog>;
|
|
45
|
+
/** Return local model entries not still equal to the plugin's previous provider snapshot. */
|
|
46
|
+
export declare function removeManagedModels(existing: unknown, previousManaged: unknown): ModelProfile[];
|
|
47
|
+
/**
|
|
48
|
+
* Three-way merge a new provider snapshot over the plugin's previous snapshot.
|
|
49
|
+
* User-added rows, per-field edits, and explicit row deletions all survive refreshes.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mergeModelCatalog(existing: unknown, previousManaged: unknown, discovered: readonly ModelProfile[], previousSuppressed?: unknown): MergedModelCatalog;
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/** Dynamic ChatGPT Codex model discovery and catalog ownership helpers. */
|
|
2
|
+
const MODELS_ENDPOINT = 'https://chatgpt.com/backend-api/codex/models';
|
|
3
|
+
/**
|
|
4
|
+
* Codex releases replace their workspace's 0.0.0 version at build time, while
|
|
5
|
+
* the backend treats the exact sentinel as an ungated account catalog. This
|
|
6
|
+
* plugin is not a Codex CLI release, so it deliberately keeps the sentinel
|
|
7
|
+
* instead of pretending to implement an arbitrary future Codex version.
|
|
8
|
+
*/
|
|
9
|
+
const CLIENT_VERSION = '0.0.0';
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
11
|
+
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
12
|
+
const MODEL_INPUTS = new Set(['text', 'image']);
|
|
13
|
+
const REASONING_EFFORTS = new Set([
|
|
14
|
+
'off',
|
|
15
|
+
'minimal',
|
|
16
|
+
'low',
|
|
17
|
+
'medium',
|
|
18
|
+
'high',
|
|
19
|
+
'xhigh',
|
|
20
|
+
'max',
|
|
21
|
+
]);
|
|
22
|
+
function recordOf(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
function nonEmptyString(value) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return undefined;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
32
|
+
}
|
|
33
|
+
function positiveInteger(value) {
|
|
34
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
function normalizeReasoning(value) {
|
|
37
|
+
if (!Array.isArray(value))
|
|
38
|
+
return undefined;
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const candidate of value) {
|
|
41
|
+
const raw = nonEmptyString(typeof candidate === 'string' ? candidate : recordOf(candidate)?.effort);
|
|
42
|
+
const effort = raw === 'none' ? 'off' : raw;
|
|
43
|
+
if (effort !== undefined && REASONING_EFFORTS.has(effort)) {
|
|
44
|
+
result[effort] = effort === 'off' ? null : effort;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const efforts = Object.keys(result);
|
|
48
|
+
if (efforts.length === 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
// llm-pi-ai rejects an object with only `off`; `false` is its canonical
|
|
51
|
+
// non-reasoning profile representation.
|
|
52
|
+
return efforts.length === 1 && efforts[0] === 'off' ? false : result;
|
|
53
|
+
}
|
|
54
|
+
function normalizeInputs(value) {
|
|
55
|
+
if (!Array.isArray(value))
|
|
56
|
+
return undefined;
|
|
57
|
+
const inputs = [];
|
|
58
|
+
for (const candidate of value) {
|
|
59
|
+
if (typeof candidate !== 'string' || !MODEL_INPUTS.has(candidate))
|
|
60
|
+
continue;
|
|
61
|
+
const input = candidate;
|
|
62
|
+
if (!inputs.includes(input))
|
|
63
|
+
inputs.push(input);
|
|
64
|
+
}
|
|
65
|
+
return inputs.length > 0 ? inputs : undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert the authenticated Codex model response into llm-pi-ai model profiles.
|
|
69
|
+
* Only picker-visible models are adopted; malformed and duplicate rows are ignored.
|
|
70
|
+
*/
|
|
71
|
+
export function parseOpenAIModelCatalog(value) {
|
|
72
|
+
const root = recordOf(value);
|
|
73
|
+
if (root === null || !Array.isArray(root.models))
|
|
74
|
+
throw new Error('ChatGPT 模型服务返回了无效数据');
|
|
75
|
+
const candidates = [];
|
|
76
|
+
const seenIds = new Set();
|
|
77
|
+
const adoptedIds = new Set();
|
|
78
|
+
for (let index = 0; index < root.models.length; index++) {
|
|
79
|
+
const raw = recordOf(root.models[index]);
|
|
80
|
+
if (raw === null)
|
|
81
|
+
continue;
|
|
82
|
+
const id = nonEmptyString(raw.slug);
|
|
83
|
+
if (id === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
seenIds.add(id);
|
|
86
|
+
if (raw.visibility !== 'list' || adoptedIds.has(id))
|
|
87
|
+
continue;
|
|
88
|
+
const model = { id };
|
|
89
|
+
const name = nonEmptyString(raw.display_name);
|
|
90
|
+
const contextWindow = positiveInteger(raw.context_window) ?? positiveInteger(raw.max_context_window);
|
|
91
|
+
const input = normalizeInputs(raw.input_modalities);
|
|
92
|
+
const reasoningEfforts = normalizeReasoning(raw.supported_reasoning_levels);
|
|
93
|
+
if (name !== undefined)
|
|
94
|
+
model.name = name;
|
|
95
|
+
if (contextWindow !== undefined)
|
|
96
|
+
model.contextWindow = contextWindow;
|
|
97
|
+
if (input !== undefined)
|
|
98
|
+
model.input = input;
|
|
99
|
+
if (reasoningEfforts !== undefined)
|
|
100
|
+
model.reasoningEfforts = reasoningEfforts;
|
|
101
|
+
adoptedIds.add(id);
|
|
102
|
+
candidates.push({
|
|
103
|
+
model,
|
|
104
|
+
priority: typeof raw.priority === 'number' && Number.isFinite(raw.priority) ? raw.priority : Number.MAX_SAFE_INTEGER,
|
|
105
|
+
index,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
candidates.sort((left, right) => left.priority - right.priority || left.index - right.index);
|
|
109
|
+
const models = candidates.map((candidate) => candidate.model);
|
|
110
|
+
if (models.length === 0)
|
|
111
|
+
throw new Error('ChatGPT 当前没有返回可用模型');
|
|
112
|
+
return { models, seenIds: [...seenIds] };
|
|
113
|
+
}
|
|
114
|
+
function abortableSignal(parent, timeoutMs) {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
let timeoutReached = false;
|
|
117
|
+
const onAbort = () => controller.abort(parent?.reason);
|
|
118
|
+
if (parent?.aborted)
|
|
119
|
+
onAbort();
|
|
120
|
+
else
|
|
121
|
+
parent?.addEventListener('abort', onAbort, { once: true });
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
timeoutReached = true;
|
|
124
|
+
controller.abort(new Error('model discovery timed out'));
|
|
125
|
+
}, timeoutMs);
|
|
126
|
+
return {
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
timedOut: () => timeoutReached,
|
|
129
|
+
dispose: () => {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
parent?.removeEventListener('abort', onAbort);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
class ResponseTooLargeError extends Error {
|
|
136
|
+
}
|
|
137
|
+
async function readBoundedResponse(response, signal) {
|
|
138
|
+
if (response.body === null)
|
|
139
|
+
return '';
|
|
140
|
+
const reader = response.body.getReader();
|
|
141
|
+
const decoder = new TextDecoder();
|
|
142
|
+
let total = 0;
|
|
143
|
+
let text = '';
|
|
144
|
+
const cancel = () => { void reader.cancel(signal.reason).catch(() => { }); };
|
|
145
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
146
|
+
try {
|
|
147
|
+
while (true) {
|
|
148
|
+
if (signal.aborted)
|
|
149
|
+
throw signal.reason;
|
|
150
|
+
const chunk = await reader.read();
|
|
151
|
+
if (chunk.done)
|
|
152
|
+
break;
|
|
153
|
+
total += chunk.value.byteLength;
|
|
154
|
+
if (total > MAX_RESPONSE_BYTES)
|
|
155
|
+
throw new ResponseTooLargeError();
|
|
156
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
157
|
+
}
|
|
158
|
+
if (signal.aborted)
|
|
159
|
+
throw signal.reason;
|
|
160
|
+
return text + decoder.decode();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
await reader.cancel().catch(() => { });
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
signal.removeEventListener('abort', cancel);
|
|
168
|
+
reader.releaseLock();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Fetch the live model catalog available to this exact ChatGPT account. */
|
|
172
|
+
export async function discoverOpenAIModels(credential, options = {}) {
|
|
173
|
+
if (nonEmptyString(credential.access) === undefined)
|
|
174
|
+
throw new Error('ChatGPT 授权不可用,请重新登录');
|
|
175
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
176
|
+
if (typeof fetcher !== 'function')
|
|
177
|
+
throw new Error('当前运行环境无法连接 ChatGPT 模型服务');
|
|
178
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
179
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
180
|
+
throw new TypeError('timeoutMs must be positive');
|
|
181
|
+
const control = abortableSignal(options.signal, timeoutMs);
|
|
182
|
+
try {
|
|
183
|
+
const url = new URL(MODELS_ENDPOINT);
|
|
184
|
+
url.searchParams.set('client_version', CLIENT_VERSION);
|
|
185
|
+
const headers = new Headers({
|
|
186
|
+
accept: 'application/json',
|
|
187
|
+
authorization: `Bearer ${credential.access}`,
|
|
188
|
+
originator: 'dsh-openai-subscription',
|
|
189
|
+
'user-agent': 'dsh-openai-subscription',
|
|
190
|
+
});
|
|
191
|
+
const accountId = nonEmptyString(credential.accountId);
|
|
192
|
+
if (accountId !== undefined)
|
|
193
|
+
headers.set('chatgpt-account-id', accountId);
|
|
194
|
+
let response;
|
|
195
|
+
try {
|
|
196
|
+
response = await fetcher(url, { method: 'GET', headers, signal: control.signal, redirect: 'error' });
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
if (options.signal?.aborted)
|
|
200
|
+
throw new Error('模型同步已取消');
|
|
201
|
+
if (control.timedOut())
|
|
202
|
+
throw new Error('获取 ChatGPT 模型超时,请稍后重试');
|
|
203
|
+
throw new Error('无法连接 ChatGPT 模型服务,请检查网络后重试');
|
|
204
|
+
}
|
|
205
|
+
if (response.status === 401 || response.status === 403) {
|
|
206
|
+
throw new Error('ChatGPT 授权已失效,请刷新授权后重试');
|
|
207
|
+
}
|
|
208
|
+
if (response.status === 429)
|
|
209
|
+
throw new Error('模型同步请求过于频繁,请稍后重试');
|
|
210
|
+
if (!response.ok)
|
|
211
|
+
throw new Error(`ChatGPT 模型服务暂时不可用(HTTP ${response.status})`);
|
|
212
|
+
const contentLength = Number(response.headers.get('content-length'));
|
|
213
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
|
|
214
|
+
throw new Error('ChatGPT 模型服务返回的数据过大');
|
|
215
|
+
}
|
|
216
|
+
let text;
|
|
217
|
+
try {
|
|
218
|
+
text = await readBoundedResponse(response, control.signal);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error instanceof ResponseTooLargeError)
|
|
222
|
+
throw new Error('ChatGPT 模型服务返回的数据过大');
|
|
223
|
+
if (options.signal?.aborted)
|
|
224
|
+
throw new Error('模型同步已取消');
|
|
225
|
+
if (control.timedOut())
|
|
226
|
+
throw new Error('获取 ChatGPT 模型超时,请稍后重试');
|
|
227
|
+
throw new Error('读取 ChatGPT 模型数据失败,请稍后重试');
|
|
228
|
+
}
|
|
229
|
+
let body;
|
|
230
|
+
try {
|
|
231
|
+
body = JSON.parse(text);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
throw new Error('ChatGPT 模型服务返回了无效数据');
|
|
235
|
+
}
|
|
236
|
+
return parseOpenAIModelCatalog(body);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
control.dispose();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function modelProfiles(value) {
|
|
243
|
+
if (!Array.isArray(value))
|
|
244
|
+
return [];
|
|
245
|
+
const result = [];
|
|
246
|
+
const seen = new Set();
|
|
247
|
+
for (const candidate of value) {
|
|
248
|
+
const record = recordOf(candidate);
|
|
249
|
+
const id = nonEmptyString(record?.id);
|
|
250
|
+
if (record === null || id === undefined || seen.has(id))
|
|
251
|
+
continue;
|
|
252
|
+
seen.add(id);
|
|
253
|
+
result.push({ ...record, id });
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
function jsonEqual(left, right) {
|
|
258
|
+
if (Object.is(left, right))
|
|
259
|
+
return true;
|
|
260
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
261
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
262
|
+
return false;
|
|
263
|
+
return left.every((value, index) => jsonEqual(value, right[index]));
|
|
264
|
+
}
|
|
265
|
+
const leftRecord = recordOf(left);
|
|
266
|
+
const rightRecord = recordOf(right);
|
|
267
|
+
if (leftRecord === null || rightRecord === null)
|
|
268
|
+
return false;
|
|
269
|
+
const leftKeys = Object.keys(leftRecord).sort();
|
|
270
|
+
const rightKeys = Object.keys(rightRecord).sort();
|
|
271
|
+
return leftKeys.length === rightKeys.length
|
|
272
|
+
&& leftKeys.every((key, index) => key === rightKeys[index] && jsonEqual(leftRecord[key], rightRecord[key]));
|
|
273
|
+
}
|
|
274
|
+
/** Return local model entries not still equal to the plugin's previous provider snapshot. */
|
|
275
|
+
export function removeManagedModels(existing, previousManaged) {
|
|
276
|
+
const previous = new Map(modelProfiles(previousManaged).map((model) => [model.id, model]));
|
|
277
|
+
return modelProfiles(existing).filter((model) => {
|
|
278
|
+
const managed = previous.get(model.id);
|
|
279
|
+
return managed === undefined || !jsonEqual(model, managed);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const MISSING = Symbol('missing');
|
|
283
|
+
function own(record, key) {
|
|
284
|
+
return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : MISSING;
|
|
285
|
+
}
|
|
286
|
+
/** Merge one JSON value, retaining only fields the user changed from the old snapshot. */
|
|
287
|
+
function mergeChangedValue(base, local, next) {
|
|
288
|
+
if (local === MISSING)
|
|
289
|
+
return base === MISSING ? next : MISSING;
|
|
290
|
+
if (base === MISSING)
|
|
291
|
+
return local;
|
|
292
|
+
if (jsonEqual(local, base))
|
|
293
|
+
return next;
|
|
294
|
+
const baseRecord = recordOf(base);
|
|
295
|
+
const localRecord = recordOf(local);
|
|
296
|
+
const nextRecord = recordOf(next);
|
|
297
|
+
if (baseRecord === null || localRecord === null || nextRecord === null)
|
|
298
|
+
return local;
|
|
299
|
+
const result = {};
|
|
300
|
+
const keys = new Set([...Object.keys(baseRecord), ...Object.keys(localRecord), ...Object.keys(nextRecord)]);
|
|
301
|
+
for (const key of keys) {
|
|
302
|
+
const value = mergeChangedValue(own(baseRecord, key), own(localRecord, key), own(nextRecord, key));
|
|
303
|
+
if (value !== MISSING)
|
|
304
|
+
result[key] = value;
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
function mergeManagedModel(previous, local, next) {
|
|
309
|
+
if (local === undefined)
|
|
310
|
+
return previous === undefined ? { ...next } : null;
|
|
311
|
+
if (previous === undefined)
|
|
312
|
+
return { ...next, ...local, id: next.id };
|
|
313
|
+
const merged = mergeChangedValue(previous, local, next);
|
|
314
|
+
return merged === MISSING ? null : { ...merged, id: next.id };
|
|
315
|
+
}
|
|
316
|
+
function suppressedIds(value) {
|
|
317
|
+
if (!Array.isArray(value))
|
|
318
|
+
return new Set();
|
|
319
|
+
return new Set(value.filter((candidate) => typeof candidate === 'string' && candidate.length > 0));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Three-way merge a new provider snapshot over the plugin's previous snapshot.
|
|
323
|
+
* User-added rows, per-field edits, and explicit row deletions all survive refreshes.
|
|
324
|
+
*/
|
|
325
|
+
export function mergeModelCatalog(existing, previousManaged, discovered, previousSuppressed) {
|
|
326
|
+
const local = modelProfiles(existing);
|
|
327
|
+
const previous = modelProfiles(previousManaged);
|
|
328
|
+
const managed = modelProfiles(discovered);
|
|
329
|
+
const localById = new Map(local.map((model) => [model.id, model]));
|
|
330
|
+
const previousById = new Map(previous.map((model) => [model.id, model]));
|
|
331
|
+
const nextIds = new Set(managed.map((model) => model.id));
|
|
332
|
+
// llm-pi-ai interprets an absent or empty list as the implicit catalog, so it
|
|
333
|
+
// cannot represent "suppress every model". Treat that state as an ownership
|
|
334
|
+
// reset and re-adopt the next complete snapshot instead.
|
|
335
|
+
const hasExplicitEntries = local.length > 0;
|
|
336
|
+
const suppressed = hasExplicitEntries ? suppressedIds(previousSuppressed) : new Set();
|
|
337
|
+
if (hasExplicitEntries) {
|
|
338
|
+
// Absence from a non-empty explicit list is a durable user deletion, not
|
|
339
|
+
// permission to resurrect the row on every remote refresh.
|
|
340
|
+
for (const model of previous) {
|
|
341
|
+
if (!localById.has(model.id))
|
|
342
|
+
suppressed.add(model.id);
|
|
343
|
+
}
|
|
344
|
+
for (const id of [...suppressed]) {
|
|
345
|
+
if (localById.has(id))
|
|
346
|
+
suppressed.delete(id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const models = [];
|
|
350
|
+
for (const model of managed) {
|
|
351
|
+
if (suppressed.has(model.id))
|
|
352
|
+
continue;
|
|
353
|
+
const old = hasExplicitEntries ? previousById.get(model.id) : undefined;
|
|
354
|
+
const merged = mergeManagedModel(old, localById.get(model.id), model);
|
|
355
|
+
if (merged !== null)
|
|
356
|
+
models.push(merged);
|
|
357
|
+
}
|
|
358
|
+
for (const model of local) {
|
|
359
|
+
if (nextIds.has(model.id))
|
|
360
|
+
continue;
|
|
361
|
+
const old = previousById.get(model.id);
|
|
362
|
+
if (old === undefined || !jsonEqual(model, old))
|
|
363
|
+
models.push({ ...model });
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
models,
|
|
367
|
+
managed: managed.map((model) => ({ ...model })),
|
|
368
|
+
suppressed: [...suppressed].sort(),
|
|
369
|
+
};
|
|
370
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-openai-subscription",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "ChatGPT subscription sign-in for DeepSeek Harness via OpenAI device authorization.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"packageManager": "
|
|
6
|
+
"packageManager": "pnpm@10.6.3",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.19.0"
|
|
9
9
|
},
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"./package.json": "./package.json"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"
|
|
27
|
+
"clean": "node --eval \"for (const path of ['dist', '.test-dist']) require('node:fs').rmSync(path, { recursive: true, force: true })\"",
|
|
28
|
+
"build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
28
29
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
29
|
-
"test": "
|
|
30
|
-
"check": "
|
|
31
|
-
"prepack": "
|
|
30
|
+
"test": "node --eval \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\" && tsc -p tsconfig.test.json && node --test .test-dist/tests/*.test.js && node --eval \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
|
|
31
|
+
"check": "node scripts/run-scripts.mjs typecheck build test",
|
|
32
|
+
"prepack": "node scripts/run-scripts.mjs check"
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
34
35
|
"dist",
|
|
@@ -63,6 +64,8 @@
|
|
|
63
64
|
"client": {
|
|
64
65
|
"platform": "web",
|
|
65
66
|
"inject": [
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
68
|
+
"@deepseek-ai/dsh-client-locale",
|
|
66
69
|
"@deepseek-ai/dsh-client-connection"
|
|
67
70
|
]
|
|
68
71
|
}
|