dsh-llm-adapters 1.2.1 → 1.2.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/DEVELOPMENT.md +66 -15
- package/FAQ.md +39 -8
- package/README.md +11 -10
- package/client.js +1802 -1822
- package/lib/index.js +181 -205
- package/lib/types/account-store.d.ts +10 -3
- package/lib/types/auth.d.ts +7 -0
- package/lib/types/codearts-oauth.d.ts +19 -0
- package/lib/types/codearts.d.ts +1 -5
- package/lib/types/config.d.ts +0 -5
- package/lib/types/index.d.ts +1 -4
- package/lib/types/raccoon.d.ts +79 -6
- package/package.json +1 -1
- package/src/account-store.ts +11 -3
- package/src/auth.ts +15 -1
- package/src/codearts-oauth.ts +36 -5
- package/src/codearts.ts +24 -29
- package/src/config.ts +258 -264
- package/src/index.ts +1 -26
- package/src/raccoon.ts +163 -50
- package/lib/types/codearts-window.d.ts +0 -65
- package/src/codearts-window.ts +0 -136
package/lib/types/auth.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export interface JwtFacts {
|
|
|
44
44
|
phone?: string;
|
|
45
45
|
ownerType?: string;
|
|
46
46
|
sessionId?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The `sub` claim, which is what an account is called on the Raccoon gateway.
|
|
49
|
+
*
|
|
50
|
+
* Unlike the token itself it survives a re-sign, so it is the seed a stable
|
|
51
|
+
* per-account session id is derived from.
|
|
52
|
+
*/
|
|
53
|
+
subject?: string;
|
|
47
54
|
}
|
|
48
55
|
/** Read non-secret JWT claims; malformed tokens simply yield no facts. */
|
|
49
56
|
export declare function decodeJwtFacts(token: string): JwtFacts;
|
|
@@ -28,6 +28,17 @@ export interface CodeArtsCredential {
|
|
|
28
28
|
dpopPrivateJwk?: DpopPrivateJwk;
|
|
29
29
|
/** Display only, from the refresh token's profile; never used for signing. */
|
|
30
30
|
userName?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The id the gateway counts concurrent sessions against (`x-ot-session-id`).
|
|
33
|
+
*
|
|
34
|
+
* Measured on a real account: the three-session cap is keyed by this header,
|
|
35
|
+
* not by `Chat-Id` / `Session-Id`. A per-request value makes every call a new
|
|
36
|
+
* session, so the account spends its own budget on its own history and the
|
|
37
|
+
* fourth call of the minute comes back `TM.00001041`. Derived from the account
|
|
38
|
+
* identity instead of stored at random: the same value in every process, and
|
|
39
|
+
* one that survives a token renewal.
|
|
40
|
+
*/
|
|
41
|
+
otSessionId?: string;
|
|
31
42
|
}
|
|
32
43
|
/** Who the credential belongs to, as the portal encoded it in the token. */
|
|
33
44
|
export interface CodeArtsUserProfile {
|
|
@@ -75,6 +86,14 @@ export declare function readJwtExpiry(token: string): number | undefined;
|
|
|
75
86
|
* request; without it the field stays empty and the page says so.
|
|
76
87
|
*/
|
|
77
88
|
export declare function readUserProfile(token: string | undefined): CodeArtsUserProfile | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* The session id this account should chat under, derived from who the account is.
|
|
91
|
+
*
|
|
92
|
+
* 32 lowercase hex, the shape the IDE's own kernel sends. Nothing but stability
|
|
93
|
+
* matters here — the gateway uses the value as a key, not as an identity — so it
|
|
94
|
+
* is hashed from the principal id rather than kept in a file.
|
|
95
|
+
*/
|
|
96
|
+
export declare function otSessionIdOf(profile: CodeArtsUserProfile | undefined): string | undefined;
|
|
78
97
|
/** The portal URL the browser opens; the callback is on loopback. */
|
|
79
98
|
export declare function buildAuthorizeUrl(port: number, pkce: PkcePair, ticketId: string): string;
|
|
80
99
|
/** Where the browser lands after a successful or failed exchange. */
|
package/lib/types/codearts.d.ts
CHANGED
|
@@ -19,7 +19,6 @@ import type { AccountStore } from './account-store.ts';
|
|
|
19
19
|
import { CodeArtsAuth } from './auth.ts';
|
|
20
20
|
import type { AuthStatus } from './auth.ts';
|
|
21
21
|
import type { ProviderModel } from './catalog.ts';
|
|
22
|
-
import { CodeArtsSessionWindow } from './codearts-window.ts';
|
|
23
22
|
import type { LoginStatus } from './login-page.ts';
|
|
24
23
|
import type { OpenRouterLookup } from './openrouter.ts';
|
|
25
24
|
import type { CheckinStatus, QuotaPack, QuotaSnapshot } from './types.ts';
|
|
@@ -72,8 +71,6 @@ export interface CodeArtsProviderOptions {
|
|
|
72
71
|
loginTimeoutMs?: number;
|
|
73
72
|
/** Injectable OpenRouter lookup for tests; defaults to the live catalog. */
|
|
74
73
|
openRouter?: OpenRouterLookup;
|
|
75
|
-
/** Chat budget; defaults to the process-wide window (survives hot reloads). */
|
|
76
|
-
window?: CodeArtsSessionWindow;
|
|
77
74
|
}
|
|
78
75
|
/** One roster entry before it becomes a model row. */
|
|
79
76
|
export interface CodeArtsModelRow {
|
|
@@ -110,7 +107,6 @@ export declare class CodeArtsProvider {
|
|
|
110
107
|
/** Models that answered `InferHub.4004.200`: no allowance pack to bill. */
|
|
111
108
|
private readonly noBenefit;
|
|
112
109
|
private noBenefitLoaded;
|
|
113
|
-
private readonly window;
|
|
114
110
|
private readonly openRouter;
|
|
115
111
|
private readonly sessionId;
|
|
116
112
|
/**
|
|
@@ -159,7 +155,7 @@ export declare class CodeArtsProvider {
|
|
|
159
155
|
* so a conversation pays for the description once.
|
|
160
156
|
*/
|
|
161
157
|
private relayImages;
|
|
162
|
-
/** Ask the vision model to describe one image
|
|
158
|
+
/** Ask the vision model to describe one image; another turn on the same session. */
|
|
163
159
|
private describeImage;
|
|
164
160
|
stream(options: GenerateOptions, model: string, readImage: ReadImage | undefined, onInvalidate: () => void): AsyncGenerator<StreamChunk>;
|
|
165
161
|
private send;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -45,8 +45,6 @@ export interface Config {
|
|
|
45
45
|
catalogFetchTimeoutSeconds?: number;
|
|
46
46
|
/** Minutes between automatic OpenRouter syncs; 0 disables the schedule. */
|
|
47
47
|
openRouterSyncMinutes?: number;
|
|
48
|
-
/** Minutes between Raccoon daily-point checks; 0 disables the monitor. */
|
|
49
|
-
raccoonCheckinMinutes?: number;
|
|
50
48
|
}
|
|
51
49
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
52
50
|
loomyEnabled: z<boolean, boolean, "volatile-defined">;
|
|
@@ -82,7 +80,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
82
80
|
modelCacheTtlSeconds: z<number, number, "volatile-defined">;
|
|
83
81
|
catalogFetchTimeoutSeconds: z<number, number, "volatile-defined">;
|
|
84
82
|
openRouterSyncMinutes: z<number, number, "volatile-defined">;
|
|
85
|
-
raccoonCheckinMinutes: z<number, number, "volatile-defined">;
|
|
86
83
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
87
84
|
loomyEnabled: z<boolean, boolean, "volatile-defined">;
|
|
88
85
|
raccoonEnabled: z<boolean, boolean, "volatile-defined">;
|
|
@@ -117,7 +114,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
117
114
|
modelCacheTtlSeconds: z<number, number, "volatile-defined">;
|
|
118
115
|
catalogFetchTimeoutSeconds: z<number, number, "volatile-defined">;
|
|
119
116
|
openRouterSyncMinutes: z<number, number, "volatile-defined">;
|
|
120
|
-
raccoonCheckinMinutes: z<number, number, "volatile-defined">;
|
|
121
117
|
}>>, "plain">;
|
|
122
118
|
export interface ResolvedConfig {
|
|
123
119
|
loomyEnabled: boolean;
|
|
@@ -153,7 +149,6 @@ export interface ResolvedConfig {
|
|
|
153
149
|
modelCacheTtlMs: number;
|
|
154
150
|
catalogFetchTimeoutMs: number;
|
|
155
151
|
openRouterSyncMinutes: number;
|
|
156
|
-
raccoonCheckinMinutes: number;
|
|
157
152
|
}
|
|
158
153
|
export type ParsedConfig = {
|
|
159
154
|
readonly [K in keyof Config]?: unknown;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -71,11 +71,10 @@ export { CodeArtsProvider, CODEARTS_DEFAULT_EFFORT, CODEARTS_EFFORTS, clampMaxTo
|
|
|
71
71
|
export type { CodeArtsActivity, CodeArtsModelRow, CodeArtsPackageInfo, CodeArtsProviderOptions, } from './codearts.ts';
|
|
72
72
|
export { CODEARTS_BENEFIT_FALLBACK, CODEARTS_CHAT_URL, CODEARTS_CONTEXT_WINDOWS, CODEARTS_EFFORT_LEVELS, CODEARTS_FALLBACK_CONTEXT_WINDOW, CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_SNAP_BASE, CODEARTS_VISION_MODELS, codeartsContextWindow, } from './codearts-endpoints.ts';
|
|
73
73
|
export { headersForFetch, signRequestHuawei } from './codearts-sign.ts';
|
|
74
|
-
export { RefreshTokenExpiredError, buildAuthorizeUrl, exchangeAuthorizationCode, exchangeRefreshToken, generateDpopKeyPair, generatePkcePair, readJwtExpiry, signDpopJws, } from './codearts-oauth.ts';
|
|
74
|
+
export { RefreshTokenExpiredError, buildAuthorizeUrl, exchangeAuthorizationCode, exchangeRefreshToken, generateDpopKeyPair, generatePkcePair, otSessionIdOf, readJwtExpiry, signDpopJws, } from './codearts-oauth.ts';
|
|
75
75
|
export type { CodeArtsCredential, DpopKeyPair, DpopPrivateJwk, PkcePair } from './codearts-oauth.ts';
|
|
76
76
|
export { CodeArtsLogin } from './codearts-login.ts';
|
|
77
77
|
export type { CodeArtsLoginAccount, CodeArtsLoginOptions } from './codearts-login.ts';
|
|
78
|
-
export { CODEARTS_SESSION_LIMIT, CODEARTS_SESSION_WINDOW_MS, CodeArtsSessionWindow, sharedSessionWindow, } from './codearts-window.ts';
|
|
79
78
|
export { ProviderHttpError, buildMessages, buildTools, consumeOpenAiStream, openAiStream, sseEvents } from './wire.ts';
|
|
80
79
|
export type { WireMessage } from './wire.ts';
|
|
81
80
|
export { API_PREFIX, buildRows } from './api.ts';
|
|
@@ -118,7 +117,6 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
118
117
|
modelCacheTtlSeconds: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
119
118
|
catalogFetchTimeoutSeconds: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
120
119
|
openRouterSyncMinutes: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
121
|
-
raccoonCheckinMinutes: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
122
120
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
123
121
|
loomyEnabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
124
122
|
raccoonEnabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
@@ -153,6 +151,5 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
153
151
|
modelCacheTtlSeconds: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
154
152
|
catalogFetchTimeoutSeconds: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
155
153
|
openRouterSyncMinutes: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
156
|
-
raccoonCheckinMinutes: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
157
154
|
}>>, "plain">;
|
|
158
155
|
export declare function apply(ctx: Context, config: ParsedConfig): void;
|
package/lib/types/raccoon.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { ModelCatalog } from './catalog.ts';
|
|
|
15
15
|
import type { ProviderModel } from './catalog.ts';
|
|
16
16
|
import type { OpenRouterLookup } from './openrouter.ts';
|
|
17
17
|
import type { CheckinStatus, QuotaSnapshot } from './types.ts';
|
|
18
|
+
import { ProviderHttpError } from './wire.ts';
|
|
18
19
|
import type { ReadImage } from './wire.ts';
|
|
19
20
|
export declare const RACCOON_BASE = "https://xiaohuanxiong.com/api/web";
|
|
20
21
|
export declare const RACCOON_CLIENT_VERSION = "v1.0.35";
|
|
@@ -30,7 +31,68 @@ export interface RaccoonProviderOptions {
|
|
|
30
31
|
fetchImpl?: typeof fetch;
|
|
31
32
|
/** Injectable OpenRouter lookup for tests; defaults to the live catalog. */
|
|
32
33
|
openRouter?: OpenRouterLookup;
|
|
34
|
+
/** Injectable session-id bookkeeping for tests; defaults to the real one. */
|
|
35
|
+
sessionIds?: RaccoonSessionIds;
|
|
33
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The upstream code for the account's token-throughput wall.
|
|
39
|
+
*
|
|
40
|
+
* It reads like a request-rate complaint (`您的模型请求频率超限`) and is not
|
|
41
|
+
* one: measured on a live account, ten large requests in ten seconds hit it
|
|
42
|
+
* while fifty small ones did not, and the window refills by throughput rather
|
|
43
|
+
* than by request count. The gateway sends no `Retry-After` and no
|
|
44
|
+
* `x-ratelimit-*` header, so nothing here can compute a wait either.
|
|
45
|
+
*/
|
|
46
|
+
export declare const RACCOON_RATE_LIMIT_CODE = "user_model_rate_limited";
|
|
47
|
+
/**
|
|
48
|
+
* Whether a rejection is the token wall rather than a generic failure.
|
|
49
|
+
*
|
|
50
|
+
* Matched on the status, the code and the wording, because the same condition
|
|
51
|
+
* arrives as an HTTP 429 and as an error frame inside a 200 stream.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isRaccoonRateLimited(status: number | undefined, text: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The wall, as an error the caller can tell apart from a generic 429.
|
|
56
|
+
*
|
|
57
|
+
* A subclass rather than a flag: the in-band path and the HTTP path would
|
|
58
|
+
* otherwise both be caught by the same handler, and the second pass would wrap
|
|
59
|
+
* the message the first pass had already rewritten.
|
|
60
|
+
*/
|
|
61
|
+
export declare class RaccoonRateLimitedError extends ProviderHttpError {
|
|
62
|
+
constructor(message: string);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One conversation's session id, stable across the turns of that conversation.
|
|
66
|
+
*
|
|
67
|
+
* The official client never opens a session per request — measured on its own
|
|
68
|
+
* logs, 125 calls over 26 minutes all carried one `sess-0-53927d74` — while an
|
|
69
|
+
* implementation that mints a fresh id per request tells the gateway that one
|
|
70
|
+
* account opened hundreds of sessions within minutes.
|
|
71
|
+
*
|
|
72
|
+
* The id is a pure function of (account, model) rather than a random value, so a
|
|
73
|
+
* restarted or hot-reloaded process continues the same conversation instead of
|
|
74
|
+
* presenting a new session, and two models stay two conversations. The principal
|
|
75
|
+
* itself never leaves the process — only a truncated digest of it does.
|
|
76
|
+
*/
|
|
77
|
+
export declare class RaccoonSessionIds {
|
|
78
|
+
private readonly principal;
|
|
79
|
+
/**
|
|
80
|
+
* @param principal - a value that is stable for one account and identifies it
|
|
81
|
+
* without being the token itself (the JWT's `sub`).
|
|
82
|
+
*/
|
|
83
|
+
constructor(principal: () => Promise<string | undefined>);
|
|
84
|
+
for(model: string): Promise<string | undefined>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `sess-<sequence>-<8 hex>`, the shape the official client sends.
|
|
88
|
+
*
|
|
89
|
+
* The sequence is the official client's process-local counter and is pinned to 0
|
|
90
|
+
* here on purpose: an incrementing value would make one conversation look like a
|
|
91
|
+
* different session after a restart, which is the one thing this id exists to
|
|
92
|
+
* prevent. Identity lives in the digest, which is truncated on purpose — the id
|
|
93
|
+
* must separate accounts without naming them on the wire.
|
|
94
|
+
*/
|
|
95
|
+
export declare function raccoonSessionId(principal: string, model: string): string;
|
|
34
96
|
/** Whether the gateway accepts a token harvested from the browser. */
|
|
35
97
|
export declare function raccoonAccessTokenValid(token: string, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
36
98
|
/**
|
|
@@ -43,9 +105,9 @@ export declare class RaccoonProvider {
|
|
|
43
105
|
readonly catalog: ModelCatalog;
|
|
44
106
|
private quotaCache;
|
|
45
107
|
private quotaInflight;
|
|
46
|
-
private checkinInflight;
|
|
47
108
|
private loginFlow;
|
|
48
109
|
private readonly openRouter;
|
|
110
|
+
private readonly sessionIds;
|
|
49
111
|
constructor(options: RaccoonProviderOptions);
|
|
50
112
|
status(): Promise<import("./auth.ts").AuthStatus>;
|
|
51
113
|
signedIn(): Promise<boolean>;
|
|
@@ -58,6 +120,14 @@ export declare class RaccoonProvider {
|
|
|
58
120
|
private login;
|
|
59
121
|
logoutAccount(): Promise<void>;
|
|
60
122
|
private headers;
|
|
123
|
+
/**
|
|
124
|
+
* The chat headers, including the session id the gateway counts sessions by.
|
|
125
|
+
*
|
|
126
|
+
* Only chat carries it: the catalogue, quota and check-in routes are plain
|
|
127
|
+
* business calls the official client sends outside any session, so leaving
|
|
128
|
+
* them alone keeps this change to the one route it was measured on.
|
|
129
|
+
*/
|
|
130
|
+
private chatHeaders;
|
|
61
131
|
/** Run one JSON call; a rejected token means this login needs a new scan. */
|
|
62
132
|
private authorizedJson;
|
|
63
133
|
private fetchModels;
|
|
@@ -76,12 +146,15 @@ export declare class RaccoonProvider {
|
|
|
76
146
|
* `point_grant_popups` on `setting_info`; the bills list is the durable proof.
|
|
77
147
|
*/
|
|
78
148
|
private dailyGrant;
|
|
79
|
-
checkinStatus(): Promise<CheckinStatus>;
|
|
80
|
-
claimCheckin(): Promise<CheckinStatus>;
|
|
81
149
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
150
|
+
* Read today's grant. **Read-only on purpose.**
|
|
151
|
+
*
|
|
152
|
+
* The gateway has no claim endpoint: points are issued on the day's first
|
|
153
|
+
* website visit. Replaying that visit (`setting_info`) can trigger the grant
|
|
154
|
+
* from here, but doing so takes a state-changing action on the account without
|
|
155
|
+
* the user asking for it in that moment, so the plugin only reports. Claiming
|
|
156
|
+
* is the website's job, and the card links there.
|
|
84
157
|
*/
|
|
85
|
-
|
|
158
|
+
checkinStatus(): Promise<CheckinStatus>;
|
|
86
159
|
stream(options: GenerateOptions, model: string, readImage: ReadImage | undefined, onUnauthorized?: () => void): AsyncGenerator<StreamChunk>;
|
|
87
160
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-llm-adapters",
|
|
3
3
|
"displayName": "Local LLM Adapters",
|
|
4
4
|
"description": "DeepSeek Harness adapters for Loomy, SenseTime Raccoon and Trae. Sign in through the provider’s own web page (WeChat QR for Loomy and Raccoon, a dedicated browser window for Trae), or reuse a signed-in local Trae client for the mainland check-in, then use the account’s models with quota, thinking levels and per-model settings pages.",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.2",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "k1-h2",
|
package/src/account-store.ts
CHANGED
|
@@ -93,12 +93,19 @@ export interface CodeArtsAccountRecord {
|
|
|
93
93
|
d: string
|
|
94
94
|
}
|
|
95
95
|
userName?: string
|
|
96
|
+
/**
|
|
97
|
+
* The `x-ot-session-id` this account chats under: one stable value instead of
|
|
98
|
+
* one per request, because the gateway counts concurrent sessions by it.
|
|
99
|
+
*
|
|
100
|
+
* A record written before the field existed derives the same value from the
|
|
101
|
+
* refresh token's profile, so an existing sign-in needs no new login.
|
|
102
|
+
*/
|
|
103
|
+
otSessionId?: string
|
|
96
104
|
/**
|
|
97
105
|
* Models the account answered `InferHub.4004.200` for.
|
|
98
106
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* call cannot succeed.
|
|
107
|
+
* Worth persisting: the account has no allowance pack for them, so without the
|
|
108
|
+
* verdict every call would pay for a request that cannot succeed.
|
|
102
109
|
*/
|
|
103
110
|
noBenefit?: string[]
|
|
104
111
|
loginAt: number
|
|
@@ -181,6 +188,7 @@ function codeartsRecord(value: unknown): CodeArtsAccountRecord | undefined {
|
|
|
181
188
|
...text(raw.codeVerifier) === undefined ? {} : { codeVerifier: text(raw.codeVerifier)! },
|
|
182
189
|
...key === undefined ? {} : { dpopPrivateJwk: key },
|
|
183
190
|
...text(raw.userName) === undefined ? {} : { userName: text(raw.userName)! },
|
|
191
|
+
...text(raw.otSessionId) === undefined ? {} : { otSessionId: text(raw.otSessionId)! },
|
|
184
192
|
...noBenefit.length === 0 ? {} : { noBenefit },
|
|
185
193
|
}
|
|
186
194
|
}
|
package/src/auth.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
AccountStore, CodeArtsAccountRecord, LoomyAccountRecord, RaccoonAccountRecord, TraeAccountRecord,
|
|
15
15
|
} from './account-store.ts'
|
|
16
16
|
import type { TraeRegion } from './brand.ts'
|
|
17
|
-
import { RefreshTokenExpiredError, exchangeRefreshToken, keyPairFromStoredJwk, readUserProfile } from './codearts-oauth.ts'
|
|
17
|
+
import { RefreshTokenExpiredError, exchangeRefreshToken, keyPairFromStoredJwk, otSessionIdOf, readUserProfile } from './codearts-oauth.ts'
|
|
18
18
|
import type { CodeArtsCredential } from './codearts-oauth.ts'
|
|
19
19
|
import { responseError } from './http.ts'
|
|
20
20
|
import { TRAE_REGION_PROFILES, TRAE_WEB_USER_AGENT } from './trae-endpoints.ts'
|
|
@@ -114,6 +114,13 @@ export interface JwtFacts {
|
|
|
114
114
|
phone?: string
|
|
115
115
|
ownerType?: string
|
|
116
116
|
sessionId?: string
|
|
117
|
+
/**
|
|
118
|
+
* The `sub` claim, which is what an account is called on the Raccoon gateway.
|
|
119
|
+
*
|
|
120
|
+
* Unlike the token itself it survives a re-sign, so it is the seed a stable
|
|
121
|
+
* per-account session id is derived from.
|
|
122
|
+
*/
|
|
123
|
+
subject?: string
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/** Normalize seconds, milliseconds, and ISO strings to epoch milliseconds. */
|
|
@@ -145,6 +152,7 @@ export function decodeJwtFacts(token: string): JwtFacts {
|
|
|
145
152
|
...typeof record.phone === 'string' ? { phone: record.phone } : {},
|
|
146
153
|
...typeof record.owner_type === 'string' ? { ownerType: record.owner_type } : {},
|
|
147
154
|
...typeof record.sid === 'string' ? { sessionId: record.sid } : {},
|
|
155
|
+
...typeof record.sub === 'string' ? { subject: record.sub } : {},
|
|
148
156
|
}
|
|
149
157
|
} catch {
|
|
150
158
|
return {}
|
|
@@ -546,6 +554,7 @@ export class CodeArtsAuth {
|
|
|
546
554
|
...next.expiresAt === undefined ? {} : { expiresAt: next.expiresAt },
|
|
547
555
|
...next.refreshToken === undefined ? {} : { refreshToken: next.refreshToken },
|
|
548
556
|
...next.userName === undefined ? {} : { userName: next.userName },
|
|
557
|
+
...next.otSessionId === undefined ? {} : { otSessionId: next.otSessionId },
|
|
549
558
|
})
|
|
550
559
|
this.lastError = undefined
|
|
551
560
|
return next
|
|
@@ -570,6 +579,10 @@ export class CodeArtsAuth {
|
|
|
570
579
|
}
|
|
571
580
|
|
|
572
581
|
function credentialOf(record: CodeArtsAccountRecord): CodeArtsCredential {
|
|
582
|
+
// A record written before `otSessionId` existed derives the very value the
|
|
583
|
+
// login would have stored, so an account that is already signed in gets its
|
|
584
|
+
// single session without a new login.
|
|
585
|
+
const otSessionId = record.otSessionId ?? otSessionIdOf(readUserProfile(record.refreshToken))
|
|
573
586
|
return {
|
|
574
587
|
accessKeyId: record.accessKeyId,
|
|
575
588
|
secretAccessKey: record.secretAccessKey,
|
|
@@ -578,5 +591,6 @@ function credentialOf(record: CodeArtsAccountRecord): CodeArtsCredential {
|
|
|
578
591
|
...record.refreshToken === undefined ? {} : { refreshToken: record.refreshToken },
|
|
579
592
|
...record.codeVerifier === undefined ? {} : { codeVerifier: record.codeVerifier },
|
|
580
593
|
...record.dpopPrivateJwk === undefined ? {} : { dpopPrivateJwk: record.dpopPrivateJwk },
|
|
594
|
+
...otSessionId === undefined ? {} : { otSessionId },
|
|
581
595
|
}
|
|
582
596
|
}
|
package/src/codearts-oauth.ts
CHANGED
|
@@ -35,6 +35,17 @@ export interface CodeArtsCredential {
|
|
|
35
35
|
dpopPrivateJwk?: DpopPrivateJwk
|
|
36
36
|
/** Display only, from the refresh token's profile; never used for signing. */
|
|
37
37
|
userName?: string
|
|
38
|
+
/**
|
|
39
|
+
* The id the gateway counts concurrent sessions against (`x-ot-session-id`).
|
|
40
|
+
*
|
|
41
|
+
* Measured on a real account: the three-session cap is keyed by this header,
|
|
42
|
+
* not by `Chat-Id` / `Session-Id`. A per-request value makes every call a new
|
|
43
|
+
* session, so the account spends its own budget on its own history and the
|
|
44
|
+
* fourth call of the minute comes back `TM.00001041`. Derived from the account
|
|
45
|
+
* identity instead of stored at random: the same value in every process, and
|
|
46
|
+
* one that survives a token renewal.
|
|
47
|
+
*/
|
|
48
|
+
otSessionId?: string
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
/** Who the credential belongs to, as the portal encoded it in the token. */
|
|
@@ -180,6 +191,19 @@ export function readUserProfile(token: string | undefined): CodeArtsUserProfile
|
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The session id this account should chat under, derived from who the account is.
|
|
196
|
+
*
|
|
197
|
+
* 32 lowercase hex, the shape the IDE's own kernel sends. Nothing but stability
|
|
198
|
+
* matters here — the gateway uses the value as a key, not as an identity — so it
|
|
199
|
+
* is hashed from the principal id rather than kept in a file.
|
|
200
|
+
*/
|
|
201
|
+
export function otSessionIdOf(profile: CodeArtsUserProfile | undefined): string | undefined {
|
|
202
|
+
const seed = profile?.userId ?? profile?.userName
|
|
203
|
+
if (seed === undefined) return undefined
|
|
204
|
+
return createHash('sha256').update(`codearts-ot-session:${seed}`).digest('hex').slice(0, 32)
|
|
205
|
+
}
|
|
206
|
+
|
|
183
207
|
/** The portal URL the browser opens; the callback is on loopback. */
|
|
184
208
|
export function buildAuthorizeUrl(port: number, pkce: PkcePair, ticketId: string): string {
|
|
185
209
|
return `${CODEARTS_PORTAL_AUTHORIZE}?theme=2&locale=zh-cn`
|
|
@@ -263,13 +287,18 @@ export async function exchangeRefreshToken(
|
|
|
263
287
|
}, keyPair, fetcher)
|
|
264
288
|
const credential = credentialFrom(token, codeVerifier, keyPair)
|
|
265
289
|
// A refresh response may omit the rotated refresh token; keep the old one so
|
|
266
|
-
// the next refresh is still possible. The name
|
|
267
|
-
//
|
|
268
|
-
|
|
290
|
+
// the next refresh is still possible. The name and the account identity ride on
|
|
291
|
+
// that token, so both are read from whichever one this credential ends up
|
|
292
|
+
// carrying: a response without a profile must not hand the account a new
|
|
293
|
+
// session id, or every renewal would cost it a slot again.
|
|
294
|
+
const previous = readUserProfile(refreshToken)
|
|
295
|
+
const userName = credential.userName ?? previous?.userName
|
|
296
|
+
const otSessionId = credential.otSessionId ?? otSessionIdOf(previous)
|
|
269
297
|
return {
|
|
270
298
|
...credential,
|
|
271
299
|
refreshToken: credential.refreshToken ?? refreshToken,
|
|
272
300
|
...userName === undefined ? {} : { userName },
|
|
301
|
+
...otSessionId === undefined ? {} : { otSessionId },
|
|
273
302
|
}
|
|
274
303
|
}
|
|
275
304
|
|
|
@@ -277,14 +306,16 @@ function credentialFrom(token: TokenResponse, codeVerifier: string, keyPair: Dpo
|
|
|
277
306
|
const credentials = token.credentials ?? {}
|
|
278
307
|
const expiration = credentials.expiration
|
|
279
308
|
const expiresAt = expiration === undefined ? undefined : Date.parse(expiration)
|
|
280
|
-
const
|
|
309
|
+
const profile = readUserProfile(token.refresh_token)
|
|
310
|
+
const otSessionId = otSessionIdOf(profile)
|
|
281
311
|
return {
|
|
282
312
|
accessKeyId: credentials.access_key_id ?? '',
|
|
283
313
|
secretAccessKey: credentials.secret_access_key ?? '',
|
|
284
314
|
securityToken: credentials.security_token ?? '',
|
|
285
315
|
...expiresAt === undefined || Number.isNaN(expiresAt) ? {} : { expiresAt },
|
|
286
316
|
...token.refresh_token === undefined ? {} : { refreshToken: token.refresh_token },
|
|
287
|
-
...userName === undefined ? {} : { userName },
|
|
317
|
+
...profile?.userName === undefined ? {} : { userName: profile.userName },
|
|
318
|
+
...otSessionId === undefined ? {} : { otSessionId },
|
|
288
319
|
codeVerifier,
|
|
289
320
|
dpopPrivateJwk: keyPair.privateKeyJwk,
|
|
290
321
|
}
|
package/src/codearts.ts
CHANGED
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
} from './codearts-endpoints.ts'
|
|
47
47
|
import { CodeArtsLogin } from './codearts-login.ts'
|
|
48
48
|
import { headersForFetch, sha256Hex, signRequestHuawei } from './codearts-sign.ts'
|
|
49
|
-
import { CodeArtsSessionWindow, sharedSessionWindow } from './codearts-window.ts'
|
|
50
49
|
import type { CodeArtsCredential } from './codearts-oauth.ts'
|
|
51
50
|
import type { LoginStatus } from './login-page.ts'
|
|
52
51
|
import { parseModelName } from './model-name.ts'
|
|
@@ -115,8 +114,6 @@ export interface CodeArtsProviderOptions {
|
|
|
115
114
|
loginTimeoutMs?: number
|
|
116
115
|
/** Injectable OpenRouter lookup for tests; defaults to the live catalog. */
|
|
117
116
|
openRouter?: OpenRouterLookup
|
|
118
|
-
/** Chat budget; defaults to the process-wide window (survives hot reloads). */
|
|
119
|
-
window?: CodeArtsSessionWindow
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
/** One roster entry before it becomes a model row. */
|
|
@@ -306,20 +303,22 @@ const sleep = (ms: number, signal?: AbortSignal): Promise<void> => new Promise((
|
|
|
306
303
|
signal?.addEventListener('abort', onAbort, { once: true })
|
|
307
304
|
})
|
|
308
305
|
|
|
309
|
-
/** Retries for a gateway that is busy for reasons the
|
|
306
|
+
/** Retries for a gateway that is busy for reasons the caller cannot predict. */
|
|
310
307
|
const QUEUE_ATTEMPTS = 3
|
|
311
|
-
/**
|
|
308
|
+
/** Pause before retrying a rejected call. */
|
|
312
309
|
const QUEUE_BACKOFF_MS = 20_000
|
|
313
310
|
|
|
314
311
|
/**
|
|
315
312
|
* The message a user sees once waiting is hopeless.
|
|
316
313
|
*
|
|
317
|
-
* The raw gateway text (`TM.00001041`) reads like a bug
|
|
318
|
-
* trial plan's, and
|
|
314
|
+
* The raw gateway text (`TM.00001041`) reads like a bug. The three concurrent
|
|
315
|
+
* sessions are the trial plan's, and one plugin chatting under a single session
|
|
316
|
+
* id is not meant to reach them: the slots are being held by something else on
|
|
317
|
+
* the same account.
|
|
319
318
|
*/
|
|
320
319
|
function queueExhausted(): ProviderHttpError {
|
|
321
320
|
return new ProviderHttpError(
|
|
322
|
-
'CodeArts
|
|
321
|
+
'CodeArts 这个账号的 3 个并发会话已被占满(通常是码道 IDE 或另一台设备),请稍后再发,或关闭其它 CodeArts 会话。',
|
|
323
322
|
429,
|
|
324
323
|
)
|
|
325
324
|
}
|
|
@@ -356,7 +355,6 @@ export class CodeArtsProvider {
|
|
|
356
355
|
/** Models that answered `InferHub.4004.200`: no allowance pack to bill. */
|
|
357
356
|
private readonly noBenefit = new Set<string>()
|
|
358
357
|
private noBenefitLoaded = false
|
|
359
|
-
private readonly window: CodeArtsSessionWindow
|
|
360
358
|
private readonly openRouter: OpenRouterLookup
|
|
361
359
|
private readonly sessionId = randomUUID()
|
|
362
360
|
/**
|
|
@@ -371,7 +369,6 @@ export class CodeArtsProvider {
|
|
|
371
369
|
private imageCacheWrite: Promise<void> = Promise.resolve()
|
|
372
370
|
|
|
373
371
|
constructor(private readonly options: CodeArtsProviderOptions) {
|
|
374
|
-
this.window = options.window ?? sharedSessionWindow()
|
|
375
372
|
this.openRouter = options.openRouter ?? new OpenRouterCatalog(options.fetchImpl ?? fetch)
|
|
376
373
|
this.catalog = new ModelCatalog(() => this.loadModels(), options.ttlMs)
|
|
377
374
|
this.login = new CodeArtsLogin({
|
|
@@ -380,9 +377,7 @@ export class CodeArtsProvider {
|
|
|
380
377
|
...options.logger === undefined ? {} : { logger: options.logger },
|
|
381
378
|
save: async (account) => {
|
|
382
379
|
await options.store.saveCodearts(account as CodeArtsAccountRecord)
|
|
383
|
-
// A different account
|
|
384
|
-
// remembers belong to the old one.
|
|
385
|
-
this.window.reset()
|
|
380
|
+
// A different account has different verdicts about what it may bill.
|
|
386
381
|
this.noBenefit.clear()
|
|
387
382
|
this.invalidateModels()
|
|
388
383
|
this.invalidateQuota()
|
|
@@ -784,7 +779,7 @@ export class CodeArtsProvider {
|
|
|
784
779
|
}
|
|
785
780
|
}
|
|
786
781
|
|
|
787
|
-
/** Ask the vision model to describe one image
|
|
782
|
+
/** Ask the vision model to describe one image; another turn on the same session. */
|
|
788
783
|
private async describeImage(dataUrl: string, signal?: AbortSignal): Promise<string> {
|
|
789
784
|
const payload = JSON.stringify({
|
|
790
785
|
model: CODEARTS_VISION_MODEL,
|
|
@@ -802,8 +797,6 @@ export class CodeArtsProvider {
|
|
|
802
797
|
let authRefreshed = false
|
|
803
798
|
let queueAttempts = 0
|
|
804
799
|
for (;;) {
|
|
805
|
-
// The vision model is another chat session, so it spends the same budget.
|
|
806
|
-
const slot = await this.window.open(signal)
|
|
807
800
|
let response: Response
|
|
808
801
|
try {
|
|
809
802
|
response = await this.send(chatId, payload, CODEARTS_VISION_MODEL, false, signal)
|
|
@@ -814,10 +807,9 @@ export class CodeArtsProvider {
|
|
|
814
807
|
continue
|
|
815
808
|
}
|
|
816
809
|
if (error instanceof CodeArtsRetry && error.kind === 'queue') {
|
|
817
|
-
slot.release()
|
|
818
810
|
if (queueAttempts < QUEUE_ATTEMPTS) {
|
|
819
811
|
queueAttempts++
|
|
820
|
-
await sleep(
|
|
812
|
+
await sleep(QUEUE_BACKOFF_MS, signal)
|
|
821
813
|
continue
|
|
822
814
|
}
|
|
823
815
|
throw queueExhausted()
|
|
@@ -842,10 +834,9 @@ export class CodeArtsProvider {
|
|
|
842
834
|
}
|
|
843
835
|
} catch (error) {
|
|
844
836
|
if (error instanceof CodeArtsRetry && error.kind === 'queue') {
|
|
845
|
-
slot.release()
|
|
846
837
|
if (queueAttempts < QUEUE_ATTEMPTS) {
|
|
847
838
|
queueAttempts++
|
|
848
|
-
await sleep(
|
|
839
|
+
await sleep(QUEUE_BACKOFF_MS, signal)
|
|
849
840
|
continue
|
|
850
841
|
}
|
|
851
842
|
throw queueExhausted()
|
|
@@ -906,10 +897,6 @@ export class CodeArtsProvider {
|
|
|
906
897
|
let benefitDropped = false
|
|
907
898
|
let queueAttempts = 0
|
|
908
899
|
for (;;) {
|
|
909
|
-
// The plan allows three sessions a minute for the whole account, and a
|
|
910
|
-
// rejected call spends one just like a served one. Wait for a slot here
|
|
911
|
-
// instead of firing into `TM.00001041` and hoping a retry lands later.
|
|
912
|
-
const slot = await this.window.open(options.signal)
|
|
913
900
|
let response: Response
|
|
914
901
|
try {
|
|
915
902
|
response = await this.send(chatId, payload, model, benefitDropped, options.signal)
|
|
@@ -933,8 +920,7 @@ export class CodeArtsProvider {
|
|
|
933
920
|
}
|
|
934
921
|
if (error.kind === 'queue' && queueAttempts < QUEUE_ATTEMPTS) {
|
|
935
922
|
queueAttempts++
|
|
936
|
-
|
|
937
|
-
await sleep(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), options.signal)
|
|
923
|
+
await sleep(QUEUE_BACKOFF_MS, options.signal)
|
|
938
924
|
continue
|
|
939
925
|
}
|
|
940
926
|
if (error.kind === 'queue') throw queueExhausted()
|
|
@@ -966,8 +952,7 @@ export class CodeArtsProvider {
|
|
|
966
952
|
// safe while nothing has been emitted, or the answer would repeat.
|
|
967
953
|
if (!produced && error instanceof CodeArtsRetry && error.kind === 'queue' && queueAttempts < QUEUE_ATTEMPTS) {
|
|
968
954
|
queueAttempts++
|
|
969
|
-
|
|
970
|
-
await sleep(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), options.signal)
|
|
955
|
+
await sleep(QUEUE_BACKOFF_MS, options.signal)
|
|
971
956
|
continue
|
|
972
957
|
}
|
|
973
958
|
if (error instanceof CodeArtsRetry && error.kind === 'queue') throw queueExhausted()
|
|
@@ -997,6 +982,9 @@ export class CodeArtsProvider {
|
|
|
997
982
|
): Promise<Response> {
|
|
998
983
|
const credential = await this.credential()
|
|
999
984
|
const benefit = !benefitDropped && this.isBenefitModel(model)
|
|
985
|
+
// A credential whose token carries no identity falls back to this process's
|
|
986
|
+
// own id: still one session for the account, never one per request.
|
|
987
|
+
const otSessionId = credential.otSessionId ?? this.sessionId.replace(/-/g, '')
|
|
1000
988
|
const signed = await signRequestHuawei(
|
|
1001
989
|
credential.accessKeyId,
|
|
1002
990
|
credential.secretAccessKey,
|
|
@@ -1015,6 +1003,14 @@ export class CodeArtsProvider {
|
|
|
1015
1003
|
'chat-id': chatId,
|
|
1016
1004
|
'session-id': this.sessionId,
|
|
1017
1005
|
lang: 'en',
|
|
1006
|
+
// Measured on a real account: the gateway counts the account's concurrent
|
|
1007
|
+
// sessions by `x-ot-session-id`, not by `chat-id` / `session-id`. One stable
|
|
1008
|
+
// value is why the trial plan stops charging its own history for every
|
|
1009
|
+
// turn; the trace id stays per request, as the IDE sends it. These join the
|
|
1010
|
+
// unsigned set — `chat-id` and `session-id` already do.
|
|
1011
|
+
'x-ot-session-id': otSessionId,
|
|
1012
|
+
'user-session-id': otSessionId,
|
|
1013
|
+
'x-ot-trace-id': randomUUID().replace(/-/g, ''),
|
|
1018
1014
|
}
|
|
1019
1015
|
const timeout = AbortSignal.timeout(Math.max(this.options.timeoutMs(), 60_000))
|
|
1020
1016
|
const response = await this.fetcher(CODEARTS_CHAT_URL, {
|
|
@@ -1052,7 +1048,6 @@ export class CodeArtsProvider {
|
|
|
1052
1048
|
|
|
1053
1049
|
async logoutAccount(): Promise<void> {
|
|
1054
1050
|
await this.options.auth.logout()
|
|
1055
|
-
this.window.reset()
|
|
1056
1051
|
this.invalidateModels()
|
|
1057
1052
|
this.invalidateQuota()
|
|
1058
1053
|
}
|