dsh-plugin-subscriptions 0.4.2 → 0.5.0
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 +4 -0
- package/README.zh.md +4 -0
- package/lib/auth/rpc.d.ts +18 -1
- package/lib/auth/rpc.js +23 -3
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/ImageGenerateToolview.js +2 -2
- package/lib/client/SpeedSelect.d.ts +48 -0
- package/lib/client/SpeedSelect.js +173 -0
- package/lib/client/SubscriptionsSection.d.ts +9 -0
- package/lib/client/SubscriptionsSection.js +2 -1
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +42 -0
- package/lib/client/locales.d.ts +14 -0
- package/lib/client/locales.js +14 -0
- package/lib/client.js +585 -73
- package/lib/client.js.map +1 -1
- package/lib/index.js +113 -42
- package/lib/providers/catalog-store.js +4 -0
- package/lib/providers/claude.js +14 -4
- package/lib/providers/codex.d.ts +27 -0
- package/lib/providers/codex.js +66 -20
- package/lib/providers/common.d.ts +2 -0
- package/lib/tools/image-generate.js +3 -10
- package/package.json +10 -6
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders,
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -595,6 +595,12 @@ function readString(payload, field) {
|
|
|
595
595
|
if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
|
|
596
596
|
return value;
|
|
597
597
|
}
|
|
598
|
+
/** Validate the `setSpeed` endpoint's tier. */
|
|
599
|
+
function readSpeedTier(payload) {
|
|
600
|
+
const tier = payload.tier;
|
|
601
|
+
if (tier !== "standard" && tier !== "fast") throw new BadRequest("payload.tier must be \"standard\" or \"fast\"");
|
|
602
|
+
return tier;
|
|
603
|
+
}
|
|
598
604
|
/** Validate the `image` endpoint's payload into a full attachment reference. */
|
|
599
605
|
function readImageRef(payload) {
|
|
600
606
|
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
@@ -633,7 +639,12 @@ function readVideoName(payload) {
|
|
|
633
639
|
if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
|
|
634
640
|
return name$1;
|
|
635
641
|
}
|
|
636
|
-
|
|
642
|
+
/** Validate the session id both speed endpoints carry. */
|
|
643
|
+
function readSessionId(payload) {
|
|
644
|
+
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
645
|
+
return readString(payload, "sessionId");
|
|
646
|
+
}
|
|
647
|
+
async function dispatch(controller, speed, endpoint, payload, signal) {
|
|
637
648
|
switch (endpoint) {
|
|
638
649
|
case "status": {
|
|
639
650
|
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
|
|
@@ -654,6 +665,10 @@ async function dispatch(controller, endpoint, payload, signal) {
|
|
|
654
665
|
case "usage": return ok(await controller.usage(readProvider(payload), signal));
|
|
655
666
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
656
667
|
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
668
|
+
case "speed": return ok(await speed.speed(readSessionId(payload)));
|
|
669
|
+
case "setSpeed":
|
|
670
|
+
await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
|
|
671
|
+
return ok({ ok: true });
|
|
657
672
|
default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
658
673
|
}
|
|
659
674
|
}
|
|
@@ -661,13 +676,14 @@ async function dispatch(controller, endpoint, payload, signal) {
|
|
|
661
676
|
* Register the `/subscriptions-auth` RPC channel when a host connection exists.
|
|
662
677
|
* @param ctx - the plugin context (headless profiles have no `connection`).
|
|
663
678
|
* @param controller - the auth operations backing the endpoints.
|
|
679
|
+
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
664
680
|
*/
|
|
665
|
-
function registerAuthRpc(ctx, controller) {
|
|
681
|
+
function registerAuthRpc(ctx, controller, speed) {
|
|
666
682
|
ctx.inject(["connection"], (ctx$1) => {
|
|
667
683
|
const connection = ctx$1.get("connection");
|
|
668
684
|
ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
|
|
669
685
|
try {
|
|
670
|
-
return await dispatch(controller, endpoint, payload, signal);
|
|
686
|
+
return await dispatch(controller, speed, endpoint, payload, signal);
|
|
671
687
|
} catch (error) {
|
|
672
688
|
return failure(error);
|
|
673
689
|
}
|
|
@@ -1013,6 +1029,8 @@ function sanitizeModel(value) {
|
|
|
1013
1029
|
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
1014
1030
|
const thinkingType = raw.thinkingType;
|
|
1015
1031
|
if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
|
|
1032
|
+
const fastTier = raw.fastTier;
|
|
1033
|
+
if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
|
|
1016
1034
|
return {
|
|
1017
1035
|
id: raw.id,
|
|
1018
1036
|
name: raw.name,
|
|
@@ -1020,7 +1038,8 @@ function sanitizeModel(value) {
|
|
|
1020
1038
|
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
1021
1039
|
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
1022
1040
|
...reasoning === void 0 ? {} : { reasoning },
|
|
1023
|
-
...thinkingType === void 0 ? {} : { thinkingType }
|
|
1041
|
+
...thinkingType === void 0 ? {} : { thinkingType },
|
|
1042
|
+
...fastTier === void 0 ? {} : { fastTier }
|
|
1024
1043
|
};
|
|
1025
1044
|
}
|
|
1026
1045
|
/**
|
|
@@ -1584,6 +1603,14 @@ const CODEX_EFFORTS = [
|
|
|
1584
1603
|
const CODEX_DEFAULT_EFFORT = ReasoningEffortId("high");
|
|
1585
1604
|
/** Every gpt-5.x codex model accepts image input. */
|
|
1586
1605
|
const CODEX_MODALITIES = ["text", "image"];
|
|
1606
|
+
/**
|
|
1607
|
+
* Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
|
|
1608
|
+
* value for priority processing, mirroring codex-rs
|
|
1609
|
+
* `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
|
|
1610
|
+
* `additional_speed_tiers` entry "fast".
|
|
1611
|
+
*/
|
|
1612
|
+
const CODEX_FAST_SERVICE_TIER = "priority";
|
|
1613
|
+
const CODEX_FAST_SPEED_TIER = "fast";
|
|
1587
1614
|
/** Static codex flow facts for the OAuth flow engine. */
|
|
1588
1615
|
const codexFlow = {
|
|
1589
1616
|
callbackPath: CODEX_CALLBACK_PATH,
|
|
@@ -1795,6 +1822,14 @@ function effortName(effort) {
|
|
|
1795
1822
|
return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
1796
1823
|
}
|
|
1797
1824
|
/**
|
|
1825
|
+
* Whether a catalog entry advertises the fast tier. Mirrors codex-rs
|
|
1826
|
+
* `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
|
|
1827
|
+
* wire value, or the legacy `additional_speed_tiers` "fast" entry.
|
|
1828
|
+
*/
|
|
1829
|
+
function supportsFastTier(entry) {
|
|
1830
|
+
return (entry.service_tiers ?? []).some((tier) => tier.id === CODEX_FAST_SERVICE_TIER) || (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER);
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1798
1833
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
1799
1834
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
1800
1835
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
@@ -1821,7 +1856,7 @@ async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
1821
1856
|
...level.description === void 0 ? {} : { description: level.description }
|
|
1822
1857
|
}));
|
|
1823
1858
|
const defaultEffort = typeof entry.default_reasoning_level === "string" && entry.default_reasoning_level.length > 0 && efforts.some((effort) => effort.id === ReasoningEffortId(entry.default_reasoning_level)) ? ReasoningEffortId(entry.default_reasoning_level) : void 0;
|
|
1824
|
-
|
|
1859
|
+
const model = {
|
|
1825
1860
|
id: entry.slug,
|
|
1826
1861
|
name: typeof entry.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : entry.slug,
|
|
1827
1862
|
...typeof entry.description === "string" && entry.description.length > 0 ? { description: entry.description } : {},
|
|
@@ -1830,13 +1865,40 @@ async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
1830
1865
|
...efforts.length > 0 ? { reasoning: {
|
|
1831
1866
|
efforts,
|
|
1832
1867
|
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
1833
|
-
} } : {}
|
|
1834
|
-
|
|
1868
|
+
} } : {},
|
|
1869
|
+
...supportsFastTier(entry) ? { fastTier: true } : {}
|
|
1870
|
+
};
|
|
1871
|
+
discovered.push(model);
|
|
1835
1872
|
}
|
|
1836
1873
|
discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
|
|
1837
1874
|
if (discovered.length === 0) throw new Error(`codex models endpoint returned an empty catalog (client_version ${CODEX_CLIENT_VERSION})`);
|
|
1838
1875
|
return discovered;
|
|
1839
1876
|
}
|
|
1877
|
+
/**
|
|
1878
|
+
* The Responses request body for one generation. A fast-tier request (the
|
|
1879
|
+
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
1880
|
+
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
1881
|
+
* matching the CLI (it never sends an explicit standard tier).
|
|
1882
|
+
*/
|
|
1883
|
+
function codexRequestBody(options, resolved, fast) {
|
|
1884
|
+
return {
|
|
1885
|
+
model: options.model,
|
|
1886
|
+
instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
1887
|
+
input: resolved.input,
|
|
1888
|
+
...options.tools !== void 0 && options.tools.length > 0 ? { tools: toResponsesTools(options.tools) } : {},
|
|
1889
|
+
tool_choice: "auto",
|
|
1890
|
+
parallel_tool_calls: true,
|
|
1891
|
+
...options.reasoningEffort !== void 0 ? { reasoning: {
|
|
1892
|
+
effort: String(options.reasoningEffort),
|
|
1893
|
+
summary: "auto"
|
|
1894
|
+
} } : {},
|
|
1895
|
+
store: false,
|
|
1896
|
+
stream: true,
|
|
1897
|
+
include: ["reasoning.encrypted_content"],
|
|
1898
|
+
...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
1899
|
+
...fast ? { service_tier: CODEX_FAST_SERVICE_TIER } : {}
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1840
1902
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
1841
1903
|
var CodexAdapter = class extends LlmAdapter {
|
|
1842
1904
|
catalog;
|
|
@@ -1892,6 +1954,16 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1892
1954
|
if (!this.options.discovery) return void 0;
|
|
1893
1955
|
return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
|
|
1894
1956
|
}
|
|
1957
|
+
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
1958
|
+
async supportsFastTier(model) {
|
|
1959
|
+
return (await this.discovered(model))?.fastTier === true;
|
|
1960
|
+
}
|
|
1961
|
+
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
1962
|
+
async fastCapableModels() {
|
|
1963
|
+
if (!this.options.discovery) return [];
|
|
1964
|
+
if (await this.options.tokens.peek() === void 0) return [];
|
|
1965
|
+
return (await this.catalog.resolve(() => this.fetchCatalog()) ?? []).filter((model) => model.fastTier === true).map((model) => model.id);
|
|
1966
|
+
}
|
|
1895
1967
|
async resolveModel(provider, model) {
|
|
1896
1968
|
const discovered = await this.discovered(model);
|
|
1897
1969
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
@@ -1930,23 +2002,9 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1930
2002
|
}
|
|
1931
2003
|
}
|
|
1932
2004
|
async request(options, session, signal) {
|
|
1933
|
-
const
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
instructions: instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
1937
|
-
input,
|
|
1938
|
-
...options.tools !== void 0 && options.tools.length > 0 ? { tools: toResponsesTools(options.tools) } : {},
|
|
1939
|
-
tool_choice: "auto",
|
|
1940
|
-
parallel_tool_calls: true,
|
|
1941
|
-
...options.reasoningEffort !== void 0 ? { reasoning: {
|
|
1942
|
-
effort: String(options.reasoningEffort),
|
|
1943
|
-
summary: "auto"
|
|
1944
|
-
} } : {},
|
|
1945
|
-
store: false,
|
|
1946
|
-
stream: true,
|
|
1947
|
-
include: ["reasoning.encrypted_content"],
|
|
1948
|
-
...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {}
|
|
1949
|
-
};
|
|
2005
|
+
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
2006
|
+
const fast = this.options.speedFor !== void 0 && await this.options.speedFor(options.sessionId, options.model);
|
|
2007
|
+
const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
|
|
1950
2008
|
return fetch(CODEX_API_URL, {
|
|
1951
2009
|
method: "POST",
|
|
1952
2010
|
headers: {
|
|
@@ -2355,7 +2413,11 @@ function detectClaudeVersion() {
|
|
|
2355
2413
|
} catch {}
|
|
2356
2414
|
return CLAUDE_CLI_FALLBACK_VERSION;
|
|
2357
2415
|
}
|
|
2358
|
-
|
|
2416
|
+
let claudeCliUserAgent;
|
|
2417
|
+
function getClaudeCliUserAgent() {
|
|
2418
|
+
if (claudeCliUserAgent === void 0) claudeCliUserAgent = `claude-cli/${detectClaudeVersion()} (external, cli)`;
|
|
2419
|
+
return claudeCliUserAgent;
|
|
2420
|
+
}
|
|
2359
2421
|
const CLAUDE_BETA_FALLBACK = [
|
|
2360
2422
|
"claude-code-20250219",
|
|
2361
2423
|
"oauth-2025-04-20",
|
|
@@ -2508,7 +2570,7 @@ async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
2508
2570
|
headers: {
|
|
2509
2571
|
"authorization": `Bearer ${session.accessToken}`,
|
|
2510
2572
|
"anthropic-beta": "oauth-2025-04-20",
|
|
2511
|
-
"user-agent":
|
|
2573
|
+
"user-agent": getClaudeCliUserAgent(),
|
|
2512
2574
|
"accept": "application/json"
|
|
2513
2575
|
},
|
|
2514
2576
|
...signal === void 0 ? {} : { signal }
|
|
@@ -2560,7 +2622,7 @@ async function fetchClaudeModels(session, fetchFn = fetch) {
|
|
|
2560
2622
|
const response = await fetchFn(CLAUDE_MODELS_URL, { headers: {
|
|
2561
2623
|
"authorization": `Bearer ${session.accessToken}`,
|
|
2562
2624
|
"anthropic-version": "2023-06-01",
|
|
2563
|
-
"user-agent":
|
|
2625
|
+
"user-agent": getClaudeCliUserAgent(),
|
|
2564
2626
|
"anthropic-dangerous-direct-browser-access": "true",
|
|
2565
2627
|
"accept": "application/json"
|
|
2566
2628
|
} });
|
|
@@ -2727,7 +2789,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
2727
2789
|
"authorization": `Bearer ${session.accessToken}`,
|
|
2728
2790
|
"anthropic-version": "2023-06-01",
|
|
2729
2791
|
"anthropic-beta": CLAUDE_BETA_FLAGS,
|
|
2730
|
-
"user-agent":
|
|
2792
|
+
"user-agent": getClaudeCliUserAgent(),
|
|
2731
2793
|
"x-app": "cli",
|
|
2732
2794
|
"anthropic-dangerous-direct-browser-access": "true",
|
|
2733
2795
|
"accept": "text/event-stream",
|
|
@@ -3697,19 +3759,11 @@ function createImageGenerateTool(options) {
|
|
|
3697
3759
|
});
|
|
3698
3760
|
}
|
|
3699
3761
|
const revisedPrompt = images.find((image) => image.revisedPrompt !== void 0)?.revisedPrompt;
|
|
3700
|
-
|
|
3762
|
+
return {
|
|
3701
3763
|
paths,
|
|
3702
3764
|
...refs.length > 0 ? { images: refs } : {},
|
|
3703
3765
|
...revisedPrompt === void 0 ? {} : { revisedPrompt }
|
|
3704
3766
|
};
|
|
3705
|
-
if (exec.parent !== void 0 && refs.length > 0) exec.deferContext(createUserMessage({
|
|
3706
|
-
content: imageGenerateContent(value),
|
|
3707
|
-
source: {
|
|
3708
|
-
kind: "plugin",
|
|
3709
|
-
plugin: "dsh-plugin-subscriptions"
|
|
3710
|
-
}
|
|
3711
|
-
}));
|
|
3712
|
-
return value;
|
|
3713
3767
|
}
|
|
3714
3768
|
});
|
|
3715
3769
|
}
|
|
@@ -4192,6 +4246,8 @@ function apply(ctx, config) {
|
|
|
4192
4246
|
let claudeTokens;
|
|
4193
4247
|
let grokTokens;
|
|
4194
4248
|
const usageFetchers = {};
|
|
4249
|
+
const speedBySession = /* @__PURE__ */ new Map();
|
|
4250
|
+
let codexAdapter;
|
|
4195
4251
|
for (const provider of providers) switch (provider) {
|
|
4196
4252
|
case "codex": {
|
|
4197
4253
|
const tokens = new TokenManager({
|
|
@@ -4208,15 +4264,19 @@ function apply(ctx, config) {
|
|
|
4208
4264
|
});
|
|
4209
4265
|
codexTokens = tokens;
|
|
4210
4266
|
usageFetchers.codex = async (signal) => fetchCodexUsage(await tokens.session(), fetch, signal);
|
|
4211
|
-
|
|
4267
|
+
let adapter;
|
|
4268
|
+
adapter = new CodexAdapter({
|
|
4212
4269
|
models: catalog.codex,
|
|
4213
4270
|
streamIdleTimeoutMs,
|
|
4214
4271
|
tokens,
|
|
4215
4272
|
discovery: !overridden.has("codex"),
|
|
4216
4273
|
onWarn,
|
|
4217
4274
|
resolveAttachments,
|
|
4218
|
-
catalogStore: catalogStore("codex")
|
|
4219
|
-
|
|
4275
|
+
catalogStore: catalogStore("codex"),
|
|
4276
|
+
speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
|
|
4277
|
+
});
|
|
4278
|
+
codexAdapter = adapter;
|
|
4279
|
+
handles.set("codex", ctx.llm.registerAdapter(["codex"], adapter));
|
|
4220
4280
|
break;
|
|
4221
4281
|
}
|
|
4222
4282
|
case "claude": {
|
|
@@ -4273,7 +4333,18 @@ function apply(ctx, config) {
|
|
|
4273
4333
|
break;
|
|
4274
4334
|
}
|
|
4275
4335
|
}
|
|
4276
|
-
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers)
|
|
4336
|
+
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers), {
|
|
4337
|
+
async speed(sessionId) {
|
|
4338
|
+
return {
|
|
4339
|
+
tier: speedBySession.get(sessionId) ?? "standard",
|
|
4340
|
+
fastModels: await codexAdapter?.fastCapableModels() ?? []
|
|
4341
|
+
};
|
|
4342
|
+
},
|
|
4343
|
+
async setSpeed(sessionId, tier) {
|
|
4344
|
+
if (tier === "standard") speedBySession.delete(sessionId);
|
|
4345
|
+
else speedBySession.set(sessionId, tier);
|
|
4346
|
+
}
|
|
4347
|
+
});
|
|
4277
4348
|
if (claudeTokens !== void 0) {
|
|
4278
4349
|
const syncTimer = setInterval(() => {
|
|
4279
4350
|
claudeTokens?.session().catch(() => {});
|
|
@@ -74,6 +74,9 @@ function sanitizeModel(value) {
|
|
|
74
74
|
const thinkingType = raw.thinkingType;
|
|
75
75
|
if (thinkingType !== undefined && thinkingType !== 'enabled' && thinkingType !== 'adaptive')
|
|
76
76
|
return undefined;
|
|
77
|
+
const fastTier = raw.fastTier;
|
|
78
|
+
if (fastTier !== undefined && typeof fastTier !== 'boolean')
|
|
79
|
+
return undefined;
|
|
77
80
|
return {
|
|
78
81
|
id: raw.id,
|
|
79
82
|
name: raw.name,
|
|
@@ -82,6 +85,7 @@ function sanitizeModel(value) {
|
|
|
82
85
|
...raw.priority === undefined ? {} : { priority: raw.priority },
|
|
83
86
|
...reasoning === undefined ? {} : { reasoning },
|
|
84
87
|
...thinkingType === undefined ? {} : { thinkingType: thinkingType },
|
|
88
|
+
...fastTier === undefined ? {} : { fastTier },
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
/**
|
package/lib/providers/claude.js
CHANGED
|
@@ -36,7 +36,17 @@ export function detectClaudeVersion() {
|
|
|
36
36
|
catch { }
|
|
37
37
|
return CLAUDE_CLI_FALLBACK_VERSION;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
// Lazy + memoized: detectClaudeVersion() shells out to `claude --version`,
|
|
40
|
+
// so this must not run at module-evaluation time (it would fire for every
|
|
41
|
+
// consumer of this module regardless of whether Claude is a configured
|
|
42
|
+
// provider). Computed on first use of getClaudeCliUserAgent() instead.
|
|
43
|
+
let claudeCliUserAgent;
|
|
44
|
+
function getClaudeCliUserAgent() {
|
|
45
|
+
if (claudeCliUserAgent === undefined) {
|
|
46
|
+
claudeCliUserAgent = `claude-cli/${detectClaudeVersion()} (external, cli)`;
|
|
47
|
+
}
|
|
48
|
+
return claudeCliUserAgent;
|
|
49
|
+
}
|
|
40
50
|
export const CLAUDE_BETA_FALLBACK = [
|
|
41
51
|
'claude-code-20250219',
|
|
42
52
|
'oauth-2025-04-20',
|
|
@@ -234,7 +244,7 @@ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
234
244
|
'anthropic-beta': 'oauth-2025-04-20',
|
|
235
245
|
// Unrecognized clients are aggressively rate-limited on this endpoint,
|
|
236
246
|
// so it presents as the CLI like every other subscription request.
|
|
237
|
-
'user-agent':
|
|
247
|
+
'user-agent': getClaudeCliUserAgent(),
|
|
238
248
|
'accept': 'application/json',
|
|
239
249
|
},
|
|
240
250
|
...signal === undefined ? {} : { signal },
|
|
@@ -283,7 +293,7 @@ export async function fetchClaudeModels(session, fetchFn = fetch) {
|
|
|
283
293
|
headers: {
|
|
284
294
|
'authorization': `Bearer ${session.accessToken}`,
|
|
285
295
|
'anthropic-version': '2023-06-01',
|
|
286
|
-
'user-agent':
|
|
296
|
+
'user-agent': getClaudeCliUserAgent(),
|
|
287
297
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
288
298
|
'accept': 'application/json',
|
|
289
299
|
},
|
|
@@ -473,7 +483,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
473
483
|
'authorization': `Bearer ${session.accessToken}`,
|
|
474
484
|
'anthropic-version': '2023-06-01',
|
|
475
485
|
'anthropic-beta': CLAUDE_BETA_FLAGS,
|
|
476
|
-
'user-agent':
|
|
486
|
+
'user-agent': getClaudeCliUserAgent(),
|
|
477
487
|
'x-app': 'cli',
|
|
478
488
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
479
489
|
'accept': 'text/event-stream',
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelIn
|
|
|
8
8
|
import type { FlowSpec } from '../auth/oauth-flow.js';
|
|
9
9
|
import type { CodexSession } from '../auth/store.js';
|
|
10
10
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
|
+
import type { ResponsesRequestInput } from '../translate/responses.js';
|
|
11
12
|
import { TokenManager } from './common.js';
|
|
12
13
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
13
14
|
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
@@ -16,6 +17,15 @@ export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
|
16
17
|
export declare const CODEX_API_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
17
18
|
/** Refresh when the access token has less than this much life left. */
|
|
18
19
|
export declare const CODEX_PREEMPT_MS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
|
|
22
|
+
* value for priority processing, mirroring codex-rs
|
|
23
|
+
* `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
|
|
24
|
+
* `additional_speed_tiers` entry "fast".
|
|
25
|
+
*/
|
|
26
|
+
export declare const CODEX_FAST_SERVICE_TIER = "priority";
|
|
27
|
+
/** One session's speed choice: standard routing or the fast (priority) tier. */
|
|
28
|
+
export type CodexSpeedTier = 'standard' | 'fast';
|
|
19
29
|
/** Static codex flow facts for the OAuth flow engine. */
|
|
20
30
|
export declare const codexFlow: FlowSpec;
|
|
21
31
|
/** User identity claims decoded from a codex id token. */
|
|
@@ -98,7 +108,20 @@ export interface CodexAdapterOptions {
|
|
|
98
108
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
99
109
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
100
110
|
catalogStore?: CatalogPersistence;
|
|
111
|
+
/**
|
|
112
|
+
* Per-request speed lookup (the composer Speed toggle's host half). Returns
|
|
113
|
+
* whether this session's current choice sends the model on the fast tier;
|
|
114
|
+
* absent means every request stays on standard routing.
|
|
115
|
+
*/
|
|
116
|
+
speedFor?: (sessionId: string | undefined, model: string) => Promise<boolean> | boolean;
|
|
101
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The Responses request body for one generation. A fast-tier request (the
|
|
120
|
+
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
121
|
+
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
122
|
+
* matching the CLI (it never sends an explicit standard tier).
|
|
123
|
+
*/
|
|
124
|
+
export declare function codexRequestBody(options: GenerateOptions, resolved: ResponsesRequestInput, fast: boolean): Record<string, unknown>;
|
|
102
125
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
103
126
|
export declare class CodexAdapter extends LlmAdapter {
|
|
104
127
|
private readonly options;
|
|
@@ -117,6 +140,10 @@ export declare class CodexAdapter extends LlmAdapter {
|
|
|
117
140
|
* call — just because the TTL lapsed mid-turn.
|
|
118
141
|
*/
|
|
119
142
|
private discovered;
|
|
143
|
+
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
144
|
+
supportsFastTier(model: string): Promise<boolean>;
|
|
145
|
+
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
146
|
+
fastCapableModels(): Promise<string[]>;
|
|
120
147
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
121
148
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
122
149
|
private request;
|
package/lib/providers/codex.js
CHANGED
|
@@ -39,6 +39,14 @@ const CODEX_EFFORTS = [
|
|
|
39
39
|
const CODEX_DEFAULT_EFFORT = ReasoningEffortId('high');
|
|
40
40
|
/** Every gpt-5.x codex model accepts image input. */
|
|
41
41
|
const CODEX_MODALITIES = ['text', 'image'];
|
|
42
|
+
/**
|
|
43
|
+
* Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
|
|
44
|
+
* value for priority processing, mirroring codex-rs
|
|
45
|
+
* `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
|
|
46
|
+
* `additional_speed_tiers` entry "fast".
|
|
47
|
+
*/
|
|
48
|
+
export const CODEX_FAST_SERVICE_TIER = 'priority';
|
|
49
|
+
const CODEX_FAST_SPEED_TIER = 'fast';
|
|
42
50
|
/** Static codex flow facts for the OAuth flow engine. */
|
|
43
51
|
export const codexFlow = {
|
|
44
52
|
callbackPath: CODEX_CALLBACK_PATH,
|
|
@@ -286,6 +294,15 @@ export const CODEX_CLIENT_VERSION = '0.147.0';
|
|
|
286
294
|
function effortName(effort) {
|
|
287
295
|
return effort === 'xhigh' ? 'Extra High' : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
288
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Whether a catalog entry advertises the fast tier. Mirrors codex-rs
|
|
299
|
+
* `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
|
|
300
|
+
* wire value, or the legacy `additional_speed_tiers` "fast" entry.
|
|
301
|
+
*/
|
|
302
|
+
function supportsFastTier(entry) {
|
|
303
|
+
return (entry.service_tiers ?? []).some(tier => tier.id === CODEX_FAST_SERVICE_TIER)
|
|
304
|
+
|| (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER);
|
|
305
|
+
}
|
|
289
306
|
/**
|
|
290
307
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
291
308
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
@@ -328,7 +345,7 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
328
345
|
&& efforts.some(effort => effort.id === ReasoningEffortId(entry.default_reasoning_level))
|
|
329
346
|
? ReasoningEffortId(entry.default_reasoning_level)
|
|
330
347
|
: undefined;
|
|
331
|
-
|
|
348
|
+
const model = {
|
|
332
349
|
id: entry.slug,
|
|
333
350
|
name: typeof entry.display_name === 'string' && entry.display_name.length > 0
|
|
334
351
|
? entry.display_name
|
|
@@ -343,7 +360,9 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
343
360
|
...efforts.length > 0
|
|
344
361
|
? { reasoning: { efforts, ...defaultEffort === undefined ? {} : { defaultEffort } } }
|
|
345
362
|
: {},
|
|
346
|
-
|
|
363
|
+
...supportsFastTier(entry) ? { fastTier: true } : {},
|
|
364
|
+
};
|
|
365
|
+
discovered.push(model);
|
|
347
366
|
}
|
|
348
367
|
discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
|
|
349
368
|
// An empty catalog from a 200 response means the backend gated us out (e.g.
|
|
@@ -354,6 +373,32 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
354
373
|
}
|
|
355
374
|
return discovered;
|
|
356
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* The Responses request body for one generation. A fast-tier request (the
|
|
378
|
+
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
379
|
+
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
380
|
+
* matching the CLI (it never sends an explicit standard tier).
|
|
381
|
+
*/
|
|
382
|
+
export function codexRequestBody(options, resolved, fast) {
|
|
383
|
+
return {
|
|
384
|
+
model: options.model,
|
|
385
|
+
instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
386
|
+
input: resolved.input,
|
|
387
|
+
...options.tools !== undefined && options.tools.length > 0
|
|
388
|
+
? { tools: toResponsesTools(options.tools) }
|
|
389
|
+
: {},
|
|
390
|
+
tool_choice: 'auto',
|
|
391
|
+
parallel_tool_calls: true,
|
|
392
|
+
...options.reasoningEffort !== undefined
|
|
393
|
+
? { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } }
|
|
394
|
+
: {},
|
|
395
|
+
store: false,
|
|
396
|
+
stream: true,
|
|
397
|
+
include: ['reasoning.encrypted_content'],
|
|
398
|
+
...options.sessionId !== undefined ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
399
|
+
...fast ? { service_tier: CODEX_FAST_SERVICE_TIER } : {},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
357
402
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
358
403
|
export class CodexAdapter extends LlmAdapter {
|
|
359
404
|
options;
|
|
@@ -423,6 +468,22 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
423
468
|
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
424
469
|
return models?.find(entry => entry.id === model);
|
|
425
470
|
}
|
|
471
|
+
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
472
|
+
async supportsFastTier(model) {
|
|
473
|
+
return (await this.discovered(model))?.fastTier === true;
|
|
474
|
+
}
|
|
475
|
+
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
476
|
+
async fastCapableModels() {
|
|
477
|
+
if (!this.options.discovery)
|
|
478
|
+
return [];
|
|
479
|
+
// Not logged in → no fast models, so the Speed toggle hides after logout
|
|
480
|
+
// (mirrors the listModels guard above).
|
|
481
|
+
const session = await this.options.tokens.peek();
|
|
482
|
+
if (session === undefined)
|
|
483
|
+
return [];
|
|
484
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
485
|
+
return (models ?? []).filter(model => model.fastTier === true).map(model => model.id);
|
|
486
|
+
}
|
|
426
487
|
async resolveModel(provider, model) {
|
|
427
488
|
// Discovered metadata (when discovery is on) wins over the static entry;
|
|
428
489
|
// the static entry wins over the built-in defaults.
|
|
@@ -465,24 +526,9 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
465
526
|
}
|
|
466
527
|
async request(options, session, signal) {
|
|
467
528
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
instructions: instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
472
|
-
input,
|
|
473
|
-
...options.tools !== undefined && options.tools.length > 0
|
|
474
|
-
? { tools: toResponsesTools(options.tools) }
|
|
475
|
-
: {},
|
|
476
|
-
tool_choice: 'auto',
|
|
477
|
-
parallel_tool_calls: true,
|
|
478
|
-
...options.reasoningEffort !== undefined
|
|
479
|
-
? { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } }
|
|
480
|
-
: {},
|
|
481
|
-
store: false,
|
|
482
|
-
stream: true,
|
|
483
|
-
include: ['reasoning.encrypted_content'],
|
|
484
|
-
...options.sessionId !== undefined ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
485
|
-
};
|
|
529
|
+
const fast = this.options.speedFor !== undefined
|
|
530
|
+
&& await this.options.speedFor(options.sessionId, options.model);
|
|
531
|
+
const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
|
|
486
532
|
return fetch(CODEX_API_URL, {
|
|
487
533
|
method: 'POST',
|
|
488
534
|
headers: {
|
|
@@ -177,6 +177,8 @@ export interface DiscoveredModel {
|
|
|
177
177
|
};
|
|
178
178
|
/** Claude-specific: which extended-thinking wire shape this model accepts. */
|
|
179
179
|
thinkingType?: 'enabled' | 'adaptive';
|
|
180
|
+
/** Codex-specific: the catalog advertises a fast (priority) service tier. */
|
|
181
|
+
fastTier?: boolean;
|
|
180
182
|
}
|
|
181
183
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
182
184
|
export declare const DISCOVERY_TTL_MS: number;
|
|
@@ -16,7 +16,6 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
16
16
|
import { basename, join } from 'node:path';
|
|
17
17
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
18
18
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
19
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
20
19
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
21
20
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
22
21
|
/** Endpoint the codex generation request is posted to. */
|
|
@@ -334,15 +333,9 @@ export function createImageGenerateTool(options) {
|
|
|
334
333
|
...refs.length > 0 ? { images: refs } : {},
|
|
335
334
|
...revisedPrompt === undefined ? {} : { revisedPrompt },
|
|
336
335
|
};
|
|
337
|
-
// Nested (Code Mode) dispatches
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
if (exec.parent !== undefined && refs.length > 0) {
|
|
341
|
-
exec.deferContext(createUserMessage({
|
|
342
|
-
content: imageGenerateContent(value),
|
|
343
|
-
source: { kind: 'plugin', plugin: 'dsh-plugin-subscriptions' },
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
336
|
+
// Nested (Code Mode) dispatches need no defer here: the harness's code
|
|
337
|
+
// mode already defers any image-bearing sub-result as a user message, so
|
|
338
|
+
// deferring again would inject the same attachment twice.
|
|
346
339
|
return value;
|
|
347
340
|
},
|
|
348
341
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Use ChatGPT (Codex), Claude, and Grok (X Premium) subscriptions as DeepSeek Harness LLM providers, with OAuth login from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,12 @@
|
|
|
48
48
|
]
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc && tsdown",
|
|
53
|
+
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/",
|
|
54
|
+
"prepare": "tsdown -c tsdown.prepare.config.ts",
|
|
55
|
+
"prepublishOnly": "pnpm build && pnpm test"
|
|
56
|
+
},
|
|
51
57
|
"peerDependencies": {
|
|
52
58
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
59
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.5",
|
|
@@ -64,6 +70,8 @@
|
|
|
64
70
|
"@deepseek-ai/dsh-client-locale": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/locale",
|
|
65
71
|
"@deepseek-ai/dsh-client-runtime": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/runtime",
|
|
66
72
|
"@deepseek-ai/dsh-client-ui-settings": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-settings",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-conversation": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-conversation",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-commands": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-commands",
|
|
67
75
|
"@deepseek-ai/dsh-client-ui-slots": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-slots",
|
|
68
76
|
"@deepseek-ai/dsh-home-paths": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/util/home-paths",
|
|
69
77
|
"@deepseek-ai/dsh-host-apiproxy": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/host/apiproxy",
|
|
@@ -75,9 +83,5 @@
|
|
|
75
83
|
"react": "^18.2.0",
|
|
76
84
|
"tsdown": "^0.15.0",
|
|
77
85
|
"typescript": "^5.8.0"
|
|
78
|
-
},
|
|
79
|
-
"scripts": {
|
|
80
|
-
"build": "tsc && tsdown",
|
|
81
|
-
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/"
|
|
82
86
|
}
|
|
83
|
-
}
|
|
87
|
+
}
|