cartethyia 2.7.6 → 2.7.7
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/.env.example +4 -2
- package/README.md +25 -1
- package/dist/dashboard/assets/{BannedPage-B1N603cM.js → BannedPage-B05aF8Dg.js} +1 -1
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-9q4_4NoK.js → CliToolDetailPage-CLUhmjUy.js} +2 -2
- package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DwshGtK-.js → CliToolsPage-CQmrqqrk.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-D762MIkp.js → CombosPage-CHG_1Wr3.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-KRYjHZsX.js → ConsoleLogPage-DPgbklad.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-d5v3K_Yc.js → InjectSystemPromptPanel-C-e2AMz6.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-Ccv4tjNS.js → LoginPage-J8wPbmfT.js} +1 -1
- package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-DN9fYNWA.js → OverviewPage-BSQnF2Lp.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +7 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-DxjvUzSD.js → ProxyPage-BBTRBAQ3.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BW4AKj2H.js → SettingsPage-D7xp5Ij_.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-44BZrxFC.js → SetupPage-LND6_Ss7.js} +1 -1
- package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-DRPavCdU.js → StudioPage-BzNnm6Bi.js} +2 -2
- package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B7M7LvMM.js → UsagePage-C1TbL24N.js} +2 -2
- package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
- package/dist/dashboard/assets/{entry-D6WvojeD.js → entry-vqGb8yVz.js} +3 -3
- package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js +1 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
- package/dist/dashboard/assets/{index-Ko4dLRnV.js → index-D36vCZht.js} +2 -2
- package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
- package/dist/dashboard/assets/{select-BHqpd-bg.js → select-C9Ssle9b.js} +1 -1
- package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +22 -22
- package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
- package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
- package/package.json +3 -1
- package/src/config/security.ts +1 -1
- package/src/config/spec.ts +13 -6
- package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
- package/src/console/cli-tools/injectors/opencode.ts +42 -14
- package/src/console/providers/catalog/contracts.ts +4 -0
- package/src/console/providers/catalog/routes.ts +1 -0
- package/src/console/providers/detail/routes.ts +8 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/quota/account-quota-tenant.ts +74 -0
- package/src/observability/telemetry-buffer.ts +2 -1
- package/src/persistence/boot.ts +13 -4
- package/src/persistence/schema.ts +7 -1
- package/src/protocol/canonical-model.ts +7 -0
- package/src/protocol/request/gemini.ts +20 -1
- package/src/protocol/tool-identity.ts +15 -1
- package/src/providers/default-registry.ts +8 -2
- package/src/providers/discovery/discovery-types.ts +18 -0
- package/src/providers/discovery/probe-phases.ts +45 -0
- package/src/providers/discovery/probing-service.ts +41 -6
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +20 -2
- package/src/providers/integrations/antigravity/antigravity.ts +24 -0
- package/src/providers/integrations/codex/codex.ts +28 -1
- package/src/providers/integrations/qoder/chat-proto.ts +328 -0
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
- package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
- package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
- package/src/providers/integrations/qoder/qoder-body.ts +78 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +5 -1
- package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
- package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
- package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
- package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
- package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
- package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
- package/src/providers/integrations/qoder/qoder-types.ts +51 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
- package/src/providers/integrations/qoder/qoder.ts +147 -1889
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-usage.ts +9 -6
- package/src/security/abuse.ts +5 -5
- package/src/transport/dispatch/upstream.ts +4 -2
- package/src/transport/routing/route-catalog.ts +8 -0
- package/src/transport/routing/route-model.ts +4 -0
- package/src/transport/routing/router.ts +8 -117
- package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +0 -1
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
- package/dist/dashboard/assets/formatters-u7JPB5BX.js +0 -1
- package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
- package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
- package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder credential resolution & PAT→jobToken exchange.
|
|
3
|
+
*
|
|
4
|
+
* Device tokens (`dt-…`) from OAuth login are used directly; a PAT
|
|
5
|
+
* (`pt-…`) must be exchanged for a short-lived job token (`jt-…`) on the
|
|
6
|
+
* realm's job-token endpoint before it can authenticate any surface.
|
|
7
|
+
*
|
|
8
|
+
* The exchange is the modern v1 endpoint (`/api/v1/jobToken/exchange`) with
|
|
9
|
+
* a plain JSON body — verified live (2026-11-05): `POST
|
|
10
|
+
* center.qoder.sh/api/v1/jobToken/exchange` with `{personal_token, machine_id}`
|
|
11
|
+
* returns `{token: jt-…, refresh_token: jrt-…, expires_at}`. The old v3
|
|
12
|
+
* scrambled-COSY endpoint is retired. `X-Request-ID` is sent uppercase and
|
|
13
|
+
* the User-Agent is `qoder/<version>` — both mirror the real client.
|
|
14
|
+
*
|
|
15
|
+
* The v1 response does not include the account's user id, so `userId`
|
|
16
|
+
* falls back to the machine id (decision 2026-11-05). That is sufficient
|
|
17
|
+
* for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
|
|
18
|
+
* as the identity placeholder until the user id is sourced another way.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
22
|
+
import { mapUpstreamHttpError } from "../../../transport/failure-policy";
|
|
23
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
24
|
+
import { qoderMachineOs } from "../../operations/cli-platform";
|
|
25
|
+
import { parseQoderOAuthState } from "./qoder-oauth";
|
|
26
|
+
import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
|
|
27
|
+
import type { QoderAuth } from "./qoder-types";
|
|
28
|
+
|
|
29
|
+
/** A PAT (`pt-…`) must be exchanged for a job token before use. */
|
|
30
|
+
export function isQoderPat(secret: string): boolean {
|
|
31
|
+
return secret.startsWith("pt-");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stable per-account machine id for OAuth dispatches. Derived from the
|
|
36
|
+
* account id (not minted per request) so pooled accounts keep one device
|
|
37
|
+
* fingerprint across restarts instead of looking like a new device every
|
|
38
|
+
* time, while staying distinct per account.
|
|
39
|
+
*/
|
|
40
|
+
export function qoderMachineIdForAccount(accountId: string): string {
|
|
41
|
+
const hex = createHash("sha256").update(`qoder-machine-v1:${accountId}`).digest("hex");
|
|
42
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds dispatch auth from an OAuth device token. The token is used
|
|
47
|
+
* directly as the COSY identity's oauth token; the user id and machine id
|
|
48
|
+
* come from the `auth_state` the login persisted. Fails closed with a
|
|
49
|
+
* reconnect prompt when the identity is absent (e.g. an account created
|
|
50
|
+
* before `auth_state` existed, or an import that dropped the field) instead
|
|
51
|
+
* of signing with a guessed id. The message names the missing field so an
|
|
52
|
+
* operator can fix the export instead of re-logging blind.
|
|
53
|
+
*/
|
|
54
|
+
export function qoderOAuthAuth(
|
|
55
|
+
secret: string,
|
|
56
|
+
authState: unknown,
|
|
57
|
+
accountId: string | undefined,
|
|
58
|
+
): QoderAuth {
|
|
59
|
+
const state = parseQoderOAuthState(authState);
|
|
60
|
+
if (state === undefined) {
|
|
61
|
+
throw new GatewayError(
|
|
62
|
+
"authentication_failed",
|
|
63
|
+
401,
|
|
64
|
+
"Qoder OAuth account is missing its user identity (request {requestId}) (auth_state.userId/machineId absent or empty — re-login, or re-export with authState included); sign in again. Wrong key, right door — check the credential 🔑",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
userId: state.userId,
|
|
69
|
+
userName: "",
|
|
70
|
+
userType: "personal_standard",
|
|
71
|
+
securityOauthToken: secret,
|
|
72
|
+
refreshToken: "",
|
|
73
|
+
machineId: state.machineId || (accountId ? qoderMachineIdForAccount(accountId) : randomUUID()),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const QODER_MACHINE_ID_TTL_MS = 60 * 60 * 1000;
|
|
78
|
+
const QODER_MAX_MACHINE_IDS = 1_024;
|
|
79
|
+
const qoderMachineIds = new Map<string, { id: string; expiresAt: number }>();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stable pseudo-device id per PAT for the whole hour (pool-friendly): a PAT
|
|
83
|
+
* keeps one machine id across requests within the TTL instead of minting a
|
|
84
|
+
* fresh device each time.
|
|
85
|
+
*/
|
|
86
|
+
function qoderMachineIdFromPat(pat: string): string {
|
|
87
|
+
const key = createHash("sha256").update(pat).digest("hex");
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const existing = qoderMachineIds.get(key);
|
|
90
|
+
if (existing && existing.expiresAt > now) return existing.id;
|
|
91
|
+
const id = randomUUID();
|
|
92
|
+
qoderMachineIds.delete(key);
|
|
93
|
+
qoderMachineIds.set(key, { id, expiresAt: now + QODER_MACHINE_ID_TTL_MS });
|
|
94
|
+
for (const [candidate, value] of qoderMachineIds) {
|
|
95
|
+
if (value.expiresAt <= now) qoderMachineIds.delete(candidate);
|
|
96
|
+
}
|
|
97
|
+
while (qoderMachineIds.size > QODER_MAX_MACHINE_IDS) {
|
|
98
|
+
const oldest = qoderMachineIds.keys().next();
|
|
99
|
+
if (oldest.done) break;
|
|
100
|
+
qoderMachineIds.delete(oldest.value);
|
|
101
|
+
}
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stable 32-hex session id for one user+model conversation. Mirrors the
|
|
107
|
+
* live IDE surface (`X-Session-ID` = sha256(userId:model), 32 hex) and is
|
|
108
|
+
* shared by the HTTP and gRPC transports so body `session_id` matches the
|
|
109
|
+
* transport-level session header.
|
|
110
|
+
*/
|
|
111
|
+
export function qoderSessionId(userId: string, modelId: string): string {
|
|
112
|
+
return createHash("sha256").update(`qoder-session:${userId}:${modelId}`).digest("hex").slice(0, 32);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
|
|
119
|
+
* modern v1 endpoint. Verified live (2026-11-05): `POST
|
|
120
|
+
* center.qoder.sh/api/v1/jobToken/exchange` with a plain JSON body returns
|
|
121
|
+
* `{token: jt-…, refresh_token: jrt-…, expires_at}`. `X-Request-ID` is
|
|
122
|
+
* sent uppercase and the User-Agent is `qoder/<version>` — both mirror the
|
|
123
|
+
* real client.
|
|
124
|
+
*
|
|
125
|
+
* The v1 response does not include the account's user id, so `userId`
|
|
126
|
+
* falls back to the machine id (decision 2026-11-05). That is sufficient
|
|
127
|
+
* for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
|
|
128
|
+
* as the identity placeholder until the user id is sourced another way.
|
|
129
|
+
*/
|
|
130
|
+
async function exchangeQoderPat(
|
|
131
|
+
pat: string,
|
|
132
|
+
signal: AbortSignal,
|
|
133
|
+
fetcher: QoderFetcher,
|
|
134
|
+
version = getQoderVersion(),
|
|
135
|
+
realm: QoderRealm = "intl",
|
|
136
|
+
): Promise<QoderAuth> {
|
|
137
|
+
const machineId = qoderMachineIdFromPat(pat);
|
|
138
|
+
const response = await fetcher(QODER_REALMS[realm].jobTokenUrl, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
"User-Agent": `qoder/${version}`,
|
|
144
|
+
"X-Request-ID": randomUUID().toUpperCase(),
|
|
145
|
+
"Cosy-ClientType": "5",
|
|
146
|
+
"Cosy-Version": version,
|
|
147
|
+
"Cosy-MachineOS": qoderMachineOs(),
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({ personal_token: pat, machine_id: machineId }),
|
|
150
|
+
signal,
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|
|
153
|
+
let responseBody: unknown;
|
|
154
|
+
try {
|
|
155
|
+
responseBody = await response.json();
|
|
156
|
+
} catch {
|
|
157
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
158
|
+
}
|
|
159
|
+
if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
|
|
160
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
161
|
+
}
|
|
162
|
+
const rec = responseBody as Record<string, unknown>;
|
|
163
|
+
const token = typeof rec["token"] === "string" ? (rec["token"] as string) : "";
|
|
164
|
+
if (!token) {
|
|
165
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
userId: machineId,
|
|
169
|
+
userName: typeof rec["name"] === "string" ? (rec["name"] as string) : "",
|
|
170
|
+
userType: typeof rec["userType"] === "string" ? (rec["userType"] as string) : "personal_standard",
|
|
171
|
+
securityOauthToken: token,
|
|
172
|
+
refreshToken: typeof rec["refresh_token"] === "string" ? (rec["refresh_token"] as string) : "",
|
|
173
|
+
machineId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the dispatch identity for one Qoder credential.
|
|
179
|
+
*
|
|
180
|
+
* Shared by the adapter and by discovery: both need the same `QoderAuth` the
|
|
181
|
+
* COSY signature is built from, and duplicating the PAT-vs-OAuth branch would
|
|
182
|
+
* let the two drift.
|
|
183
|
+
*/
|
|
184
|
+
export async function resolveQoderAuth(input: {
|
|
185
|
+
readonly secret: string;
|
|
186
|
+
readonly authState: unknown;
|
|
187
|
+
readonly signal: AbortSignal;
|
|
188
|
+
readonly fetcher: (url: string, init: RequestInit) => Promise<Response>;
|
|
189
|
+
/** Which deployment mints the job token; a PAT is realm-bound. */
|
|
190
|
+
readonly realm?: QoderRealm;
|
|
191
|
+
}): Promise<QoderAuth> {
|
|
192
|
+
const realm = input.realm ?? "intl";
|
|
193
|
+
if (isQoderPat(input.secret)) {
|
|
194
|
+
return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
|
|
195
|
+
}
|
|
196
|
+
// Discovery is handed only the secret and the persisted state, so the
|
|
197
|
+
// account id is read from that state — the same place `qoderOAuthAuth` reads
|
|
198
|
+
// it — rather than being a separate field callers can pass wrongly.
|
|
199
|
+
const state = parseQoderOAuthState(input.authState);
|
|
200
|
+
return qoderOAuthAuth(input.secret, input.authState, state?.userId);
|
|
201
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared canonical→Qoder message helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a separate module so both transports (`qoder.ts` HTTP and
|
|
5
|
+
* `qoder-grpc.ts`) build and flatten message content the same way.
|
|
6
|
+
*/
|
|
7
|
+
import type { ContentPart, ToolChoice, ToolDefinition } from "../../../protocol/canonical-model";
|
|
8
|
+
|
|
9
|
+
/** Joins text/tool-result content of one canonical message into plain text. */
|
|
10
|
+
export function flattenCanonicalContent(content: readonly ContentPart[]): string {
|
|
11
|
+
return content
|
|
12
|
+
.filter((b) => b.kind === "text" || b.kind === "toolResult")
|
|
13
|
+
.map((b) => {
|
|
14
|
+
if (b.kind === "text") return b.text;
|
|
15
|
+
if (b.kind === "toolResult") return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
16
|
+
return "";
|
|
17
|
+
})
|
|
18
|
+
.join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Projects the canonical tools onto Qoder's chat-completions `tools` array
|
|
23
|
+
* (the same OpenAI-shaped declaration both the HTTP and the gRPC surface
|
|
24
|
+
* expect).
|
|
25
|
+
*
|
|
26
|
+
* Without this the upstream is never told a tool exists, so a model that wants
|
|
27
|
+
* one improvises markup in its prose (`<Read file_path="..." />`) instead of
|
|
28
|
+
* emitting a structured call — the call is then neither runnable nor
|
|
29
|
+
* recoverable, because it never was a call on either side.
|
|
30
|
+
*
|
|
31
|
+
* Only `function` tools are representable upstream. Anything else is dropped
|
|
32
|
+
* rather than forwarded: Qoder rejects an unknown variant during
|
|
33
|
+
* deserialization and fails the whole turn, which is worse than omitting a
|
|
34
|
+
* tool the model could not have used anyway.
|
|
35
|
+
*/
|
|
36
|
+
export function qoderTools(
|
|
37
|
+
tools: readonly ToolDefinition[] | undefined,
|
|
38
|
+
): Array<Record<string, unknown>> {
|
|
39
|
+
const out: Array<Record<string, unknown>> = [];
|
|
40
|
+
for (const tool of tools ?? []) {
|
|
41
|
+
if (tool.tool_type !== undefined && tool.tool_type !== "function") continue;
|
|
42
|
+
// The upstream requires `tools[].function` to be an object. A flat
|
|
43
|
+
// declaration naming the tool at the top level is rejected outright with
|
|
44
|
+
// "'function' is a required property, expected an object", which fails the
|
|
45
|
+
// whole request rather than just this tool.
|
|
46
|
+
out.push({
|
|
47
|
+
type: "function",
|
|
48
|
+
function: {
|
|
49
|
+
name: tool.name,
|
|
50
|
+
...(tool.description === undefined ? { description: "" } : { description: tool.description }),
|
|
51
|
+
parameters: tool.jsonSchema ?? { type: "object", properties: {} },
|
|
52
|
+
...(tool.strict === undefined ? {} : { strict: tool.strict }),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Projects the canonical tool choice onto the upstream's field.
|
|
61
|
+
*
|
|
62
|
+
* The upstream only accepts a string here (both surfaces). Given the object
|
|
63
|
+
* form `{type:"function",function:{name}}` it ignores the field, and the model
|
|
64
|
+
* then calls the tool with no arguments filled in.
|
|
65
|
+
*
|
|
66
|
+
* `allowed_tools` has no chat-completions equivalent, so it collapses to the
|
|
67
|
+
* direction it already names rather than being dropped: a caller that asked for
|
|
68
|
+
* `required` must not silently get `auto`.
|
|
69
|
+
*/
|
|
70
|
+
export function qoderToolChoice(choice: ToolChoice | undefined): unknown {
|
|
71
|
+
if (choice === undefined) return undefined;
|
|
72
|
+
if (typeof choice === "string") return choice;
|
|
73
|
+
if (choice.type === "tool" || choice.type === "custom") {
|
|
74
|
+
const name = choice.name.trim();
|
|
75
|
+
return name.length > 0 ? name : "auto";
|
|
76
|
+
}
|
|
77
|
+
return choice.mode;
|
|
78
|
+
}
|
|
@@ -147,7 +147,11 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
|
|
|
147
147
|
smodel: { id: "smodel", display_name: "Sonus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
|
|
148
148
|
cmodel: { id: "cmodel", display_name: "Cantus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
|
|
149
149
|
qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
150
|
-
|
|
150
|
+
// `qfmodel` is billed, not free: upstream's live `is_free: true` for this
|
|
151
|
+
// slot is a plan-tier flag, not a zero-cost tier — a free account gets
|
|
152
|
+
// `quota exceeded` (116) when dispatching it. The only truly free slot is
|
|
153
|
+
// `lite`. Do not re-add `is_free` here.
|
|
154
|
+
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
151
155
|
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
152
156
|
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
153
157
|
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3.1-Pro", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
|