cartethyia 2.7.6 → 2.7.8
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 +26 -2
- package/dist/dashboard/assets/{BannedPage-B1N603cM.js → BannedPage-DeIbIiBU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-9q4_4NoK.js → CliToolDetailPage-BRWKPQPf.js} +2 -2
- package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DwshGtK-.js → CliToolsPage-BP-Y5iHN.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-D762MIkp.js → CombosPage-B7JHozVa.js} +1 -1
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-KRYjHZsX.js → ConsoleLogPage-GX7iGGcu.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-d5v3K_Yc.js → InjectSystemPromptPanel-Cv-2Ol_K.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-Ccv4tjNS.js → LoginPage-bKfz_b1K.js} +1 -1
- package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C6WlpU5E.js → ModelPicker-B-WQxx_H.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-DN9fYNWA.js → OverviewPage-Z9pJZFo8.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +7 -0
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-DxjvUzSD.js → ProxyPage-emlTuxz6.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BW4AKj2H.js → SettingsPage-BSy7EwGo.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-44BZrxFC.js → SetupPage-CUZrKs9r.js} +1 -1
- package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-DRPavCdU.js → StudioPage-jWmUdDpy.js} +2 -2
- package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B7M7LvMM.js → UsagePage-5F9tGVGG.js} +2 -2
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
- package/dist/dashboard/assets/app-BEsU8hKs.css +2 -0
- package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
- package/dist/dashboard/assets/{badge-BaX2GTrn.js → badge-DyI_mJfi.js} +1 -1
- package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DFH4Bkte.js → banner-B-yXtCqb.js} +1 -1
- package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
- package/dist/dashboard/assets/{card-CxDw0lK6.js → card-_HwAEqFF.js} +1 -1
- package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DfS1ntiq.js → clipboard-button-ByjCk0o9.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BK-NRr5u.js → dialog-BZhOlfz-.js} +1 -1
- package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-QqPul6sC.js → drawer-BADqIRYP.js} +1 -1
- package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BMlYJVFj.js → entry-COaC-irN.js} +1 -1
- package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
- package/dist/dashboard/assets/{entry-D6WvojeD.js → entry-Ca7LSCet.js} +3 -3
- package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Dmx7gBiN.js → entry-PDd27uCd.js} +1 -1
- package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DcHJyRiu.js +1 -0
- package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
- package/dist/dashboard/assets/{index-Ko4dLRnV.js → index-CgKmFngv.js} +2 -2
- package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
- package/dist/dashboard/assets/{inline-CHJQwxLa.js → inline-B2BirKry.js} +1 -1
- package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
- package/dist/dashboard/assets/{input-BlmlizVV.js → input-D-qV9dKY.js} +1 -1
- package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bz93I-yE.js → layout-B7tVWhtV.js} +1 -1
- package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Ml16KCfu.js → list-row-CtkGE3bF.js} +1 -1
- package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
- package/dist/dashboard/assets/{live-7_QpnYGu.js → live-DGizF06-.js} +1 -1
- package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
- package/dist/dashboard/assets/{network-CuMDaTzT.js → network-C9UE-bOr.js} +1 -1
- package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-CoqpaDMx.js → numbers-BnAd22a_.js} +1 -1
- package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-B5aOoweZ.js → pagination-BAywyMzS.js} +1 -1
- package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
- package/dist/dashboard/assets/{select-BHqpd-bg.js → select-BCOcv8WK.js} +1 -1
- package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
- package/dist/dashboard/assets/{stack-j7cXjgYX.js → stack-ByKt3Q1g.js} +1 -1
- package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHmEnLa2.js → state-DXbGTx7N.js} +1 -1
- package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DmZcyTIC.js → switch-ZeTsW623.js} +1 -1
- package/dist/dashboard/assets/switch-ZeTsW623.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 +44 -44
- 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/operations/byok-wire-profile.ts +9 -12
- 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/ModelPicker-C6WlpU5E.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/app-mweRcsKp.css +0 -2
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.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/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
- /package/dist/dashboard/assets/{app-DRnQLhsc.js → app-CTxoqyYB.js} +0 -0
- /package/dist/dashboard/assets/{app-DRnQLhsc.js.gz → app-CTxoqyYB.js.gz} +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode websearch provider plugin generation.
|
|
3
|
+
*
|
|
4
|
+
* The Console writes `websearch.provider = "<provider>"` into the user's
|
|
5
|
+
* opencode.jsonc. That value resolves to a WebSearch provider the local plugin
|
|
6
|
+
* below registers with OpenCode. The plugin reads the gateway URL and API key
|
|
7
|
+
* from the matching provider entry in opencode.jsonc — the same provider block
|
|
8
|
+
* the injector writes — so the search route follows the configured provider.
|
|
9
|
+
*
|
|
10
|
+
* OpenCode loads every `.js` file under `~/.config/opencode/plugins/`
|
|
11
|
+
* (or `<config-dir>/plugins/`) as a plugin; no registration step is needed
|
|
12
|
+
* beyond writing the file.
|
|
13
|
+
*/
|
|
14
|
+
import { ensureDir, fileExists, join, removeFile, writeTextFileWithBackup } from "./fs-ops";
|
|
15
|
+
|
|
16
|
+
const PLUGIN_FILENAME = (providerName: string) => `${providerName}-websearch.js`;
|
|
17
|
+
|
|
18
|
+
/** Escapes the provider name for use inside a JS double-quoted string. */
|
|
19
|
+
function jsStringLiteral(value: string): string {
|
|
20
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Renders the websearch plugin source for one provider name.
|
|
25
|
+
*
|
|
26
|
+
* `providerName` is the exact provider key written into opencode.jsonc (e.g.
|
|
27
|
+
* `Cartethyia`, `CarteLocal`). The plugin reads that block's `baseURL` and
|
|
28
|
+
* `apiKey` as the single source of truth, so no secret is duplicated anywhere.
|
|
29
|
+
*/
|
|
30
|
+
export function renderWebsearchPlugin(providerName: string): string {
|
|
31
|
+
const quoted = jsStringLiteral(providerName);
|
|
32
|
+
return `// ${quoted}-websearch — route OpenCode's built-in websearch tool through the
|
|
33
|
+
// Cartethyia gateway's /v1/search native route, using the "${quoted}"
|
|
34
|
+
// provider block in opencode.jsonc as the single source of truth.
|
|
35
|
+
//
|
|
36
|
+
// Generated by the Cartethyia console (CLI tools → OpenCode). Do not edit by
|
|
37
|
+
// hand; re-apply the tool config to regenerate.
|
|
38
|
+
//
|
|
39
|
+
// Env overrides (all optional):
|
|
40
|
+
// CARTETHYIA_SEARCH_URL (default: the provider's baseURL + /search)
|
|
41
|
+
// CARTETHYIA_SEARCH_API_KEY (default: the provider's apiKey)
|
|
42
|
+
// CARTETHYIA_SEARCH_BACKEND (default: "exa"; "parallel" also works)
|
|
43
|
+
|
|
44
|
+
import { Plugin } from "@opencode/plugin";
|
|
45
|
+
import { readFileSync } from "node:fs";
|
|
46
|
+
import { homedir } from "node:os";
|
|
47
|
+
import { join } from "node:path";
|
|
48
|
+
|
|
49
|
+
function pick(obj, keys) {
|
|
50
|
+
for (const k of keys) {
|
|
51
|
+
const v = obj?.[k];
|
|
52
|
+
if (v !== undefined && v !== null) return v;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Pull the "${quoted}" provider's baseURL/apiKey out of opencode.jsonc.
|
|
58
|
+
// The file is JSONC (comments allowed) so search for the provider block
|
|
59
|
+
// loosely rather than parsing strictly.
|
|
60
|
+
function readProviderSettings() {
|
|
61
|
+
const candidates = [
|
|
62
|
+
process.env.OPENCODE_CONFIG,
|
|
63
|
+
join(homedir(), ".config", "opencode", "opencode.jsonc"),
|
|
64
|
+
join(homedir(), ".config", "opencode", "opencode.json"),
|
|
65
|
+
];
|
|
66
|
+
for (const path of candidates) {
|
|
67
|
+
if (!path) continue;
|
|
68
|
+
let text;
|
|
69
|
+
try {
|
|
70
|
+
text = readFileSync(path, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Find the provider block, then the next provider block or closing brace.
|
|
75
|
+
const providerStart = text.indexOf(\`"\${PROVIDER_NAME}"\`);
|
|
76
|
+
if (providerStart < 0) continue;
|
|
77
|
+
const settingsMatch = text.slice(providerStart).match(/"settings"\\s*:\\s*\\{/);
|
|
78
|
+
if (!settingsMatch) continue;
|
|
79
|
+
const settingsStart = providerStart + settingsMatch.index + settingsMatch[0].length - 1;
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let end = -1;
|
|
82
|
+
for (let i = settingsStart; i < text.length; i++) {
|
|
83
|
+
const ch = text[i];
|
|
84
|
+
if (ch === "{") depth += 1;
|
|
85
|
+
else if (ch === "}") {
|
|
86
|
+
depth -= 1;
|
|
87
|
+
if (depth === 0) {
|
|
88
|
+
end = i;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (end < 0) continue;
|
|
94
|
+
const settings = text.slice(settingsStart, end + 1);
|
|
95
|
+
const baseURL = settings.match(/"baseURL"\\s*:\\s*"([^"]+)"/)?.[1];
|
|
96
|
+
const apiKey = settings.match(/"apiKey"\\s*:\\s*"([^"]+)"/)?.[1];
|
|
97
|
+
if (baseURL) {
|
|
98
|
+
return { baseURL, apiKey };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { baseURL: undefined, apiKey: undefined };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const PROVIDER_NAME = "${quoted}";
|
|
105
|
+
const local = readProviderSettings();
|
|
106
|
+
const BASE_URL = (process.env.CARTETHYIA_SEARCH_URL || (local.baseURL ? \`\${local.baseURL.replace(/\\/+$/, "")}/search\` : "")).replace(/\\/+$/, "");
|
|
107
|
+
const API_KEY = process.env.CARTETHYIA_SEARCH_API_KEY || local.apiKey || "";
|
|
108
|
+
const BACKEND = process.env.CARTETHYIA_SEARCH_BACKEND || "exa";
|
|
109
|
+
|
|
110
|
+
export default Plugin.define({
|
|
111
|
+
id: "${quoted}-websearch",
|
|
112
|
+
async setup(ctx) {
|
|
113
|
+
await ctx.websearch.transform((editor) => {
|
|
114
|
+
editor.add({
|
|
115
|
+
id: "${quoted}",
|
|
116
|
+
name: "Cartethyia Search",
|
|
117
|
+
execute: async ({ query }, { signal }) => {
|
|
118
|
+
if (!BASE_URL) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Cartethyia search gateway is not configured: set CARTETHYIA_SEARCH_URL or add a ${quoted} provider with baseURL in opencode.jsonc",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const response = await fetch(BASE_URL, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"content-type": "application/json",
|
|
127
|
+
...(API_KEY ? { authorization: \`Bearer \${API_KEY}\` } : {}),
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
model: BACKEND,
|
|
131
|
+
query,
|
|
132
|
+
max_results: 8,
|
|
133
|
+
}),
|
|
134
|
+
signal,
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const text = await response.text().catch(() => "");
|
|
138
|
+
throw new Error(
|
|
139
|
+
\`Cartethyia search failed (HTTP \${response.status}): \${text.slice(0, 200)}\`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const data = await response.json();
|
|
143
|
+
const results = Array.isArray(data?.results) ? data.results : [];
|
|
144
|
+
return results.map((item) => ({
|
|
145
|
+
url: String(item?.url ?? ""),
|
|
146
|
+
title: typeof item?.title === "string" && item.title ? item.title : undefined,
|
|
147
|
+
content: typeof item?.snippet === "string" && item.snippet ? item.snippet : undefined,
|
|
148
|
+
time:
|
|
149
|
+
typeof item?.published_at === "string" && item.published_at
|
|
150
|
+
? { published: Date.parse(item.published_at) || undefined }
|
|
151
|
+
: {},
|
|
152
|
+
}));
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
editor.default.set("${quoted}");
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Writes the websearch plugin for a provider into the OpenCode config dir. */
|
|
163
|
+
export async function writeWebsearchPlugin(
|
|
164
|
+
configDir: string,
|
|
165
|
+
providerName: string,
|
|
166
|
+
): Promise<string> {
|
|
167
|
+
const pluginsDir = join(configDir, "plugins");
|
|
168
|
+
await ensureDir(pluginsDir);
|
|
169
|
+
const filename = PLUGIN_FILENAME(providerName);
|
|
170
|
+
const target = join(pluginsDir, filename);
|
|
171
|
+
await writeTextFileWithBackup(target, renderWebsearchPlugin(providerName));
|
|
172
|
+
return target;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Removes the websearch plugin for a provider from the OpenCode config dir. */
|
|
176
|
+
export async function removeWebsearchPlugin(
|
|
177
|
+
configDir: string,
|
|
178
|
+
providerName: string,
|
|
179
|
+
): Promise<boolean> {
|
|
180
|
+
const target = join(configDir, "plugins", PLUGIN_FILENAME(providerName));
|
|
181
|
+
if (await fileExists(target)) {
|
|
182
|
+
await removeFile(target);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
@@ -2,6 +2,7 @@ import type {} from "../contracts";
|
|
|
2
2
|
import { ensureV1Suffix, homeDir, join, readJsonFile, writeJsonFileWithBackup } from "../fs-ops";
|
|
3
3
|
import type { InjectorSpec } from "../contracts";
|
|
4
4
|
import { DEFAULT_SDK } from "../contracts";
|
|
5
|
+
import { writeWebsearchPlugin, removeWebsearchPlugin } from "../cartethyia-websearch-plugin";
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
// OpenCode injector spec.
|
|
@@ -19,6 +20,7 @@ interface OpencodeProvider {
|
|
|
19
20
|
interface OpencodeConfig {
|
|
20
21
|
provider?: Record<string, OpencodeProvider>;
|
|
21
22
|
model?: string;
|
|
23
|
+
websearch?: { provider?: string };
|
|
22
24
|
agent?: {
|
|
23
25
|
explorer?: {
|
|
24
26
|
description?: string;
|
|
@@ -51,11 +53,14 @@ export const opencodeSpec: InjectorSpec = {
|
|
|
51
53
|
if (!config) {
|
|
52
54
|
return { configured: false, currentEndpoint: null, rawApiKey: null, currentModels: null };
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
// The provider key written by this injector is the websearch provider name
|
|
57
|
+
// when one is configured, otherwise the default `cartethyia`.
|
|
58
|
+
const providerId = config.websearch?.provider ?? "cartethyia";
|
|
59
|
+
const provider = config.provider?.[providerId];
|
|
55
60
|
const endpoint = provider?.options?.baseURL ?? null;
|
|
56
61
|
const models = provider?.models ? Object.keys(provider.models) : null;
|
|
57
62
|
const active =
|
|
58
|
-
typeof config.model === "string" && config.model.startsWith(
|
|
63
|
+
typeof config.model === "string" && config.model.startsWith(`${providerId}/`)
|
|
59
64
|
? config.model.split("/").slice(1).join("/")
|
|
60
65
|
: null;
|
|
61
66
|
return {
|
|
@@ -70,10 +75,15 @@ export const opencodeSpec: InjectorSpec = {
|
|
|
70
75
|
const existing = (await readJsonFile(path)) as OpencodeConfig | null;
|
|
71
76
|
const config: OpencodeConfig = (existing ?? {}) as OpencodeConfig;
|
|
72
77
|
|
|
78
|
+
// The provider key written into opencode.jsonc follows the websearch
|
|
79
|
+
// provider name when one is chosen, which keeps the websearch plugin and
|
|
80
|
+
// the provider block pointing at the same entry.
|
|
81
|
+
const providerId = input.websearch ?? "cartethyia";
|
|
82
|
+
|
|
73
83
|
if (!config.provider) config.provider = {};
|
|
74
|
-
const prior = config.provider
|
|
75
|
-
const otherProviders = Object.keys(config.provider).filter((id) => id !==
|
|
76
|
-
config.provider
|
|
84
|
+
const prior = config.provider[providerId];
|
|
85
|
+
const otherProviders = Object.keys(config.provider).filter((id) => id !== providerId);
|
|
86
|
+
config.provider[providerId] = {
|
|
77
87
|
npm: input.sdk ?? prior?.npm ?? DEFAULT_SDK,
|
|
78
88
|
options: {
|
|
79
89
|
...(prior?.options ?? {}),
|
|
@@ -84,7 +94,7 @@ export const opencodeSpec: InjectorSpec = {
|
|
|
84
94
|
};
|
|
85
95
|
|
|
86
96
|
const activeModel = input.activeModel ?? input.modelIds[0] ?? "";
|
|
87
|
-
config.model = activeModel === "" ? "" :
|
|
97
|
+
config.model = activeModel === "" ? "" : `${providerId}/${activeModel}`;
|
|
88
98
|
|
|
89
99
|
if (input.websearch !== undefined) {
|
|
90
100
|
config.websearch = { provider: input.websearch };
|
|
@@ -107,13 +117,20 @@ export const opencodeSpec: InjectorSpec = {
|
|
|
107
117
|
config.agent.explorer = {
|
|
108
118
|
description: "Fast explorer subagent",
|
|
109
119
|
mode: "subagent",
|
|
110
|
-
model:
|
|
120
|
+
model: `${providerId}/${subagentModel}`,
|
|
111
121
|
};
|
|
112
122
|
|
|
113
123
|
await writeJsonFileWithBackup(path, config);
|
|
124
|
+
|
|
125
|
+
// Write (or refresh) the websearch provider plugin when web search is
|
|
126
|
+
// routed through the gateway. The plugin id follows the provider name.
|
|
127
|
+
if (input.websearch !== undefined) {
|
|
128
|
+
await writeWebsearchPlugin(opencodeSpec.resolveDir?.() ?? join(homeDir(), ".config", "opencode"), providerId);
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
return otherProviders.length > 0
|
|
115
132
|
? [
|
|
116
|
-
`Config already has other provider(s): ${otherProviders.join(", ")}. They were kept as-is — only
|
|
133
|
+
`Config already has other provider(s): ${otherProviders.join(", ")}. They were kept as-is — only ${providerId} was written/updated.`,
|
|
117
134
|
]
|
|
118
135
|
: undefined;
|
|
119
136
|
},
|
|
@@ -122,29 +139,39 @@ export const opencodeSpec: InjectorSpec = {
|
|
|
122
139
|
const config = (await readJsonFile(path)) as OpencodeConfig | null;
|
|
123
140
|
if (!config) return false;
|
|
124
141
|
|
|
142
|
+
const providerId = config.websearch?.provider ?? "cartethyia";
|
|
143
|
+
|
|
125
144
|
if (config.provider) {
|
|
126
|
-
delete config.provider
|
|
145
|
+
delete config.provider[providerId];
|
|
127
146
|
if (Object.keys(config.provider).length === 0) delete config.provider;
|
|
128
147
|
}
|
|
129
148
|
|
|
130
|
-
if (typeof config.model === "string" && config.model.startsWith(
|
|
149
|
+
if (typeof config.model === "string" && config.model.startsWith(`${providerId}/`)) {
|
|
131
150
|
delete config.model;
|
|
132
151
|
}
|
|
133
152
|
|
|
134
|
-
if (config.agent?.explorer?.model?.startsWith(
|
|
153
|
+
if (config.agent?.explorer?.model?.startsWith(`${providerId}/`)) {
|
|
135
154
|
delete config.agent.explorer;
|
|
136
155
|
if (Object.keys(config.agent).length === 0) delete config.agent;
|
|
137
156
|
}
|
|
138
157
|
|
|
158
|
+
if (config.websearch) {
|
|
159
|
+
delete config.websearch;
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
await writeJsonFileWithBackup(path, config);
|
|
163
|
+
|
|
164
|
+
// Remove the generated websearch plugin when present.
|
|
165
|
+
await removeWebsearchPlugin(opencodeSpec.resolveDir?.() ?? join(homeDir(), ".config", "opencode"), providerId);
|
|
140
166
|
},
|
|
141
167
|
|
|
142
168
|
download(input) {
|
|
143
169
|
const activeModel = input.activeModel ?? input.modelIds[0] ?? "";
|
|
144
170
|
const subagentModel = input.subagentModel ?? input.modelIds[0] ?? activeModel;
|
|
171
|
+
const providerId = input.websearch ?? "cartethyia";
|
|
145
172
|
const config: OpencodeConfig = {
|
|
146
173
|
provider: {
|
|
147
|
-
|
|
174
|
+
[providerId]: {
|
|
148
175
|
npm: input.sdk ?? DEFAULT_SDK,
|
|
149
176
|
options: {
|
|
150
177
|
baseURL: ensureV1Suffix(input.endpoint),
|
|
@@ -153,12 +180,13 @@ export const opencodeSpec: InjectorSpec = {
|
|
|
153
180
|
models: buildOpencodeModels(input.modelIds),
|
|
154
181
|
},
|
|
155
182
|
},
|
|
156
|
-
model: activeModel === "" ? "" :
|
|
183
|
+
model: activeModel === "" ? "" : `${providerId}/${activeModel}`,
|
|
184
|
+
...(input.websearch !== undefined ? { websearch: { provider: input.websearch } } : {}),
|
|
157
185
|
agent: {
|
|
158
186
|
explorer: {
|
|
159
187
|
description: "Fast explorer subagent",
|
|
160
188
|
mode: "subagent",
|
|
161
|
-
model:
|
|
189
|
+
model: `${providerId}/${subagentModel}`,
|
|
162
190
|
},
|
|
163
191
|
},
|
|
164
192
|
};
|
|
@@ -935,6 +935,8 @@ export interface ProviderRoutingResponse {
|
|
|
935
935
|
readonly skipProxy: boolean;
|
|
936
936
|
/** Built-in API-key upstream User-Agent; OAuth and custom providers retain their own identity. */
|
|
937
937
|
readonly userAgent: string;
|
|
938
|
+
/** Operator-set custom User-Agent override; `null` = no override (native identity is used). */
|
|
939
|
+
readonly customUserAgent: string | null;
|
|
938
940
|
}
|
|
939
941
|
|
|
940
942
|
/**
|
|
@@ -966,4 +968,6 @@ export interface UpdateProviderRoutingRequest {
|
|
|
966
968
|
readonly skipProxy?: boolean;
|
|
967
969
|
/** Built-in API-key upstream User-Agent. */
|
|
968
970
|
readonly userAgent?: string;
|
|
971
|
+
/** Operator-set custom User-Agent override; `null` clears it back to native. */
|
|
972
|
+
readonly customUserAgent?: string | null;
|
|
969
973
|
}
|
|
@@ -126,6 +126,7 @@ const probeModelBody = t.Object({
|
|
|
126
126
|
reasoningEffort: t.Optional(literalUnion(PROBE_REASONING_EFFORTS)),
|
|
127
127
|
maxOutputTokens: t.Optional(t.Integer({ minimum: 1, maximum: 131072 })),
|
|
128
128
|
stream: t.Optional(t.Boolean()),
|
|
129
|
+
surface: t.Optional(literalUnion(["default", "cosy", "grpc", "ide"])),
|
|
129
130
|
});
|
|
130
131
|
const byokConnectionTestBody = t.Object({
|
|
131
132
|
baseUrl: t.String(),
|
|
@@ -100,6 +100,13 @@ function createProviderDetailOperations(deps: ProviderDetailConfig) {
|
|
|
100
100
|
) {
|
|
101
101
|
throw new ConsoleDomainError("invalid_request", 400, "userAgent must be a header value up to 4096 characters");
|
|
102
102
|
}
|
|
103
|
+
if (
|
|
104
|
+
patch.customUserAgent !== undefined &&
|
|
105
|
+
patch.customUserAgent !== null &&
|
|
106
|
+
(patch.customUserAgent.length > 4096 || /[\u0000-\u001f\u007f]/.test(patch.customUserAgent))
|
|
107
|
+
) {
|
|
108
|
+
throw new ConsoleDomainError("invalid_request", 400, "customUserAgent must be a header value up to 4096 characters, or null");
|
|
109
|
+
}
|
|
103
110
|
const updated = await deps.store.updateRouting(providerId, a.tenantId, patch);
|
|
104
111
|
await deps.auditSink?.record({
|
|
105
112
|
access: a,
|
|
@@ -157,6 +164,7 @@ const updateRoutingBody = t.Object({
|
|
|
157
164
|
usageFloorUnit: t.Optional(t.Union([t.Literal("credit"), t.Literal("percent")])),
|
|
158
165
|
skipProxy: t.Optional(t.Boolean()),
|
|
159
166
|
userAgent: t.Optional(t.String({ minLength: 0, maxLength: 4096 })),
|
|
167
|
+
customUserAgent: t.Optional(t.Nullable(t.String({ minLength: 0, maxLength: 4096 }))),
|
|
160
168
|
});
|
|
161
169
|
|
|
162
170
|
|
|
@@ -41,6 +41,7 @@ function mapRow(row: typeof providerRoutingSettings.$inferSelect): ProviderRouti
|
|
|
41
41
|
enabled: row.enabled,
|
|
42
42
|
skipProxy: row.skipProxy,
|
|
43
43
|
userAgent: row.userAgent,
|
|
44
|
+
customUserAgent: row.customUserAgent,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -94,6 +95,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
|
|
|
94
95
|
enabled: false,
|
|
95
96
|
skipProxy: defaultSkipProxy(providerId),
|
|
96
97
|
userAgent: buildClaudeCliUserAgent(),
|
|
98
|
+
customUserAgent: null,
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
async updateRouting(
|
|
@@ -116,6 +118,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
|
|
|
116
118
|
enabled: patch.enabled ?? false,
|
|
117
119
|
skipProxy: patch.skipProxy ?? defaultSkipProxy(providerId),
|
|
118
120
|
userAgent: patch.userAgent ?? buildClaudeCliUserAgent(),
|
|
121
|
+
customUserAgent: patch.customUserAgent ?? null,
|
|
119
122
|
};
|
|
120
123
|
const setClause: Record<string, unknown> = {};
|
|
121
124
|
if (patch.strategy !== undefined) setClause.strategy = patch.strategy;
|
|
@@ -128,6 +131,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
|
|
|
128
131
|
if (patch.enabled !== undefined) setClause.enabled = patch.enabled;
|
|
129
132
|
if (patch.skipProxy !== undefined) setClause.skipProxy = patch.skipProxy;
|
|
130
133
|
if (patch.userAgent !== undefined) setClause.userAgent = patch.userAgent;
|
|
134
|
+
if (patch.customUserAgent !== undefined) setClause.customUserAgent = patch.customUserAgent;
|
|
131
135
|
const upserted = await this.db
|
|
132
136
|
.insert(providerRoutingSettings)
|
|
133
137
|
.values(values)
|
|
@@ -48,6 +48,11 @@ import {
|
|
|
48
48
|
fetchQoderDailyCheckin,
|
|
49
49
|
isQoderCheckinProvider,
|
|
50
50
|
} from "../../providers/integrations/qoder/qoder-checkin";
|
|
51
|
+
import {
|
|
52
|
+
claimQoderCredits,
|
|
53
|
+
QODER_CREDIT_PROVIDER_IDS,
|
|
54
|
+
type QoderCreditClaimResult,
|
|
55
|
+
} from "../../providers/integrations/qoder/claim-qoder-credits";
|
|
51
56
|
import {
|
|
52
57
|
DrizzleQoderNativeIdentityStore,
|
|
53
58
|
type QoderNativeIdentityStore,
|
|
@@ -1136,6 +1141,75 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
1136
1141
|
: (result.detail ?? "Claim it in the ZCode app"),
|
|
1137
1142
|
};
|
|
1138
1143
|
})
|
|
1144
|
+
// Qoder daily-credits claim is likewise an explicit operator action: the
|
|
1145
|
+
// CREDITS campaign only surfaces with the real UMID headers, so the claim
|
|
1146
|
+
// mints/binds a native identity first (same store the check-in worker uses).
|
|
1147
|
+
.post("/accounts/:id/claim-qoder-credits", async ({ request, params, set }) => {
|
|
1148
|
+
const access = requireTenantScope(accessResolver(request), "dashboard:write");
|
|
1149
|
+
const rows = await db
|
|
1150
|
+
.select()
|
|
1151
|
+
.from(providerAccounts)
|
|
1152
|
+
.where(
|
|
1153
|
+
and(eq(providerAccounts.id, params.id), eq(providerAccounts.tenantId, access.tenantId)),
|
|
1154
|
+
)
|
|
1155
|
+
.limit(1);
|
|
1156
|
+
const account = rows[0];
|
|
1157
|
+
if (!account) {
|
|
1158
|
+
set.status = 404;
|
|
1159
|
+
return { error: `Account ${params.id} not found`, code: "not_found" };
|
|
1160
|
+
}
|
|
1161
|
+
if (!QODER_CREDIT_PROVIDER_IDS.includes(account.providerId as (typeof QODER_CREDIT_PROVIDER_IDS)[number])) {
|
|
1162
|
+
set.status = 400;
|
|
1163
|
+
return {
|
|
1164
|
+
error: "Daily credits are only claimable for qoder accounts",
|
|
1165
|
+
code: "qoder_credits_unsupported",
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
let result: QoderCreditClaimResult;
|
|
1169
|
+
try {
|
|
1170
|
+
const credential = await refreshDeps.resolveCredential(account.providerId as ProviderId, account.id);
|
|
1171
|
+
const fetcher = networkBindingFactory
|
|
1172
|
+
? await dailyCheckinEgressForPass({ db, networkBindingFactory })(account.id, access.tenantId)
|
|
1173
|
+
: globalThis.fetch;
|
|
1174
|
+
result = await claimQoderCredits({ db, providerId: account.providerId, accountId: account.id, credential, fetcher });
|
|
1175
|
+
} catch {
|
|
1176
|
+
set.status = 502;
|
|
1177
|
+
return {
|
|
1178
|
+
ok: false, status: 502,
|
|
1179
|
+
data: { id: account.id, state: "error", amount: 0, detail: "Qoder credit claim request failed" },
|
|
1180
|
+
message: "Qoder credit claim request failed", code: "qoder_credit_claim_failed",
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (result.state === "claimed") {
|
|
1184
|
+
// Cold readers must not resurrect the balance from before the grant.
|
|
1185
|
+
await db.update(providerAccounts).set({ quotaSnapshot: null }).where(
|
|
1186
|
+
and(eq(providerAccounts.id, account.id), eq(providerAccounts.tenantId, access.tenantId)),
|
|
1187
|
+
);
|
|
1188
|
+
await invalidateQuotaCacheForAccount(account.id, redis);
|
|
1189
|
+
await snapshotInvalidator?.invalidate();
|
|
1190
|
+
}
|
|
1191
|
+
if (result.state === "error") {
|
|
1192
|
+
set.status = 502;
|
|
1193
|
+
return {
|
|
1194
|
+
ok: false,
|
|
1195
|
+
status: 502,
|
|
1196
|
+
data: { id: account.id, ...result },
|
|
1197
|
+
message: result.detail ?? "Qoder credit claim failed",
|
|
1198
|
+
code: "qoder_credit_claim_failed",
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
ok: true,
|
|
1203
|
+
status: 200,
|
|
1204
|
+
data: { id: account.id, ...result },
|
|
1205
|
+
message:
|
|
1206
|
+
result.state === "nothing"
|
|
1207
|
+
? "No CREDITS campaign to claim"
|
|
1208
|
+
: result.state === "claimed"
|
|
1209
|
+
? `Claimed ${result.amount} credits`
|
|
1210
|
+
: "CREDITS campaign already claimed",
|
|
1211
|
+
};
|
|
1212
|
+
})
|
|
1139
1213
|
.post("/accounts/:id/activity-report", async ({ request, params, set }) => {
|
|
1140
1214
|
const access = requireTenantScope(accessResolver(request), "dashboard:write");
|
|
1141
1215
|
const rows = await db
|
|
@@ -153,7 +153,8 @@ function telemetryEventRow(event: TelemetryEventInput) {
|
|
|
153
153
|
event.usage?.estimated_cost === null || event.usage === undefined
|
|
154
154
|
? null
|
|
155
155
|
: String(event.usage.estimated_cost),
|
|
156
|
-
//
|
|
156
|
+
// Provider-billed credit (Tencent buddy-meter `credit`, Qoder `credits`);
|
|
157
|
+
// NULL when the upstream reported no credit field.
|
|
157
158
|
// Numeric columns are string-typed in Drizzle, like `estimatedCostUsd`.
|
|
158
159
|
creditUsed: event.usage?.credit_used != null ? String(event.usage.credit_used) : null,
|
|
159
160
|
resolveMs: event.resolveMs ?? null,
|
package/src/persistence/boot.ts
CHANGED
|
@@ -11,18 +11,27 @@ import { applySqlMigrations, createPgHandle, getPool, isPgHandle } from "./db-pg
|
|
|
11
11
|
import { applySqliteMigrations, buildSqliteHandle, createSqliteDatabase } from "./db-sqlite";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* True when `error` is a
|
|
14
|
+
* True when `error` is a unique-violation from either supported dialect:
|
|
15
|
+
*
|
|
16
|
+
* - Postgres: SQLSTATE 23505 (`code: "23505"`).
|
|
17
|
+
* - SQLite: `UNIQUE constraint failed: <index>` (message text) or the
|
|
18
|
+
* driver's `SQLITE_CONSTRAINT_UNIQUE` code (19).
|
|
15
19
|
*
|
|
16
20
|
* Lives at the persistence boundary because it is a fact about the driver's
|
|
17
21
|
* error shape, not about any one domain. It walks a bounded `cause` chain
|
|
18
|
-
* because Drizzle wraps driver errors, so the
|
|
19
|
-
* error the caller catches.
|
|
22
|
+
* because Drizzle wraps driver errors, so the code/message is not always on
|
|
23
|
+
* the error the caller catches.
|
|
20
24
|
*/
|
|
21
25
|
export function isUniqueViolation(error: unknown): boolean {
|
|
22
26
|
let current: unknown = error;
|
|
23
27
|
for (let depth = 0; depth < 3; depth += 1) {
|
|
24
28
|
if (typeof current !== "object" || current === null) return false;
|
|
25
|
-
|
|
29
|
+
const record = current as { code?: unknown; message?: unknown };
|
|
30
|
+
const code = typeof record.code === "string" ? record.code : "";
|
|
31
|
+
const message = typeof record.message === "string" ? record.message : "";
|
|
32
|
+
if (code === "23505" || code === "SQLITE_CONSTRAINT_UNIQUE") return true;
|
|
33
|
+
if (message.includes("UNIQUE constraint failed:")) return true;
|
|
34
|
+
if (code === "19" && message.includes("constraint")) return true;
|
|
26
35
|
if (!("cause" in current)) return false;
|
|
27
36
|
current = current.cause;
|
|
28
37
|
}
|
|
@@ -793,6 +793,12 @@ export const providerRoutingSettings = pgTable(
|
|
|
793
793
|
// row that never set one is `buildClaudeCliUserAgent()` in
|
|
794
794
|
// providers/operations/custom-cli-headers.ts, which this mirrors.
|
|
795
795
|
userAgent: text("user_agent").notNull().default("claude-cli/2.1.293 (external, cli)"),
|
|
796
|
+
// Operator-set custom User-Agent, stored separately from the provider's
|
|
797
|
+
// native client identity (`user_agent`). NULL = no override — dispatch
|
|
798
|
+
// uses the native identity. A non-NULL value is an explicit override
|
|
799
|
+
// applied at the request edge, even when the provider would otherwise
|
|
800
|
+
// stamp its own identity.
|
|
801
|
+
customUserAgent: text("custom_user_agent"),
|
|
796
802
|
// When true, this provider's requests always dial direct. When false,
|
|
797
803
|
// dispatch automatically selects among every active network pool the
|
|
798
804
|
// tenant owns (least-loaded/non-cooldown) — never an admin-pinned pool.
|
|
@@ -1458,7 +1464,7 @@ export const telemetryEvents = pgTable(
|
|
|
1458
1464
|
outputTokens: integer("output_tokens"),
|
|
1459
1465
|
reasoningTokens: integer("reasoning_tokens"),
|
|
1460
1466
|
estimatedCostUsd: numeric("estimated_cost_usd", { precision: 12, scale: 6 }),
|
|
1461
|
-
/** Provider-billed credits (Tencent buddy-meter `credit`); NULL when unreported. */
|
|
1467
|
+
/** Provider-billed credits (Tencent buddy-meter `credit`, Qoder `credits`); NULL when unreported. */
|
|
1462
1468
|
creditUsed: numeric("credit_used", { precision: 12, scale: 4 }),
|
|
1463
1469
|
/**
|
|
1464
1470
|
* Gateway-side time before the upstream dispatch began: routing,
|
|
@@ -370,6 +370,13 @@ export interface CanonicalTerminalEvent {
|
|
|
370
370
|
readonly response_id?: string;
|
|
371
371
|
/** Event timestamp in milliseconds since epoch (optional for profiling). */
|
|
372
372
|
readonly timestamp?: number;
|
|
373
|
+
/**
|
|
374
|
+
* Provider-specific upstream metadata about the model that actually served
|
|
375
|
+
* the turn — its native model id, supply target, tier, etc. Not a client
|
|
376
|
+
* surface: probes read it to report what the upstream really ran when the
|
|
377
|
+
* transport's model echo is an opaque id.
|
|
378
|
+
*/
|
|
379
|
+
readonly provider_metadata?: Record<string, unknown>;
|
|
373
380
|
/**
|
|
374
381
|
* Set by an adapter whose turn is not finished: the stream ended because the
|
|
375
382
|
* model asked for a tool the client must answer, and the provider's
|
|
@@ -287,7 +287,26 @@ export function buildGeminiPayload(
|
|
|
287
287
|
});
|
|
288
288
|
const payload: Record<string, unknown> = { contents };
|
|
289
289
|
if (systemParts.length > 0) payload["systemInstruction"] = { role: "user", parts: systemParts };
|
|
290
|
-
|
|
290
|
+
// Gemini's native hosted tools are declared as bare object keys, not
|
|
291
|
+
// function declarations: `{ googleSearch: {} }` (and codeExecution/urlContext)
|
|
292
|
+
// enable Google-backed grounding. Accept both spellings the gateway's search
|
|
293
|
+
// adapters use (`web_search` tool_type and `googleSearch` name) and map them
|
|
294
|
+
// to the native declaration so the upstream actually grounds the query.
|
|
295
|
+
const HOSTED_TOOL_NAMES = new Set(["googleSearch", "codeExecution", "urlContext"]);
|
|
296
|
+
const hostedTools: Record<string, Record<string, unknown>>[] = [];
|
|
297
|
+
for (const tool of request.tools ?? []) {
|
|
298
|
+
if (tool.name === "googleSearch" || tool.tool_type === "web_search") {
|
|
299
|
+
hostedTools.push({ googleSearch: {} });
|
|
300
|
+
} else if (tool.name === "codeExecution") {
|
|
301
|
+
hostedTools.push({ codeExecution: {} });
|
|
302
|
+
} else if (tool.name === "urlContext") {
|
|
303
|
+
hostedTools.push({ urlContext: {} });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const functionTools = (request.tools ?? []).filter(
|
|
307
|
+
(t) => t.name && !HOSTED_TOOL_NAMES.has(t.name) && t.tool_type !== "web_search",
|
|
308
|
+
);
|
|
309
|
+
if (hostedTools.length > 0) payload["tools"] = hostedTools;
|
|
291
310
|
if (functionTools.length > 0) {
|
|
292
311
|
payload["tools"] = [
|
|
293
312
|
{
|
|
@@ -73,9 +73,23 @@ export function trackToolCall(
|
|
|
73
73
|
fallbackIndex?: number,
|
|
74
74
|
): { index: number; isFirst: boolean; state: ToolCallState } {
|
|
75
75
|
const priorIndex = tracker.indexById.get(tool.id);
|
|
76
|
+
// A wire index is only honored when this id already owns it, or when it is
|
|
77
|
+
// not yet claimed by another call. Upstream Qoder parallel calls frequently
|
|
78
|
+
// repeat the same wire index (0,0) for different ids, which would otherwise
|
|
79
|
+
// collapse two calls into one at the encoder (`Map` keyed by index). When
|
|
80
|
+
// the wire index collides with another id, synthesize a fresh index so every
|
|
81
|
+
// logical call keeps a distinct wire position.
|
|
76
82
|
const index =
|
|
77
83
|
tool.index !== undefined && Number.isInteger(tool.index) && tool.index >= 0
|
|
78
|
-
?
|
|
84
|
+
? (() => {
|
|
85
|
+
if (priorIndex !== undefined) return priorIndex;
|
|
86
|
+
for (const [id, assigned] of tracker.indexById) {
|
|
87
|
+
if (id !== tool.id && assigned === tool.index) {
|
|
88
|
+
return tracker.nextIndex++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return tool.index;
|
|
92
|
+
})()
|
|
79
93
|
: (priorIndex ?? fallbackIndex ?? tracker.nextIndex++);
|
|
80
94
|
tracker.indexById.set(tool.id, index);
|
|
81
95
|
tracker.nextIndex = Math.max(tracker.nextIndex, index + 1);
|
|
@@ -202,7 +202,10 @@ export const PROVIDER_CAPABILITIES = {
|
|
|
202
202
|
},
|
|
203
203
|
codex: {
|
|
204
204
|
loadAdapter: async () => (await import("./integrations/codex/codex")).createCodexAdapter({ provider_id: "codex" }),
|
|
205
|
-
loadModels: async () =>
|
|
205
|
+
loadModels: async () => {
|
|
206
|
+
const { CODEX_MODELS, CODEX_SEARCH_MODELS } = await import("./integrations/codex/codex");
|
|
207
|
+
return [...CODEX_MODELS, ...CODEX_SEARCH_MODELS];
|
|
208
|
+
},
|
|
206
209
|
loadAuthentication: oauthCapability(() => import("./integrations/codex/codex-oauth"), "codexOAuthClient", { withRefresher: true }),
|
|
207
210
|
loadQuotaCollector: quotaCapability(() => import("./integrations/codex/codex-quota"), "fetchCodexQuota"),
|
|
208
211
|
loadModelDiscovery: async () => async ({ credential, fetcher }) =>
|
|
@@ -235,7 +238,10 @@ export const PROVIDER_CAPABILITIES = {
|
|
|
235
238
|
},
|
|
236
239
|
antigravity: {
|
|
237
240
|
loadAdapter: async () => (await import("./integrations/antigravity/antigravity")).antigravityAdapter,
|
|
238
|
-
loadModels: async () =>
|
|
241
|
+
loadModels: async () => {
|
|
242
|
+
const { ANTIGRAVITY_MODELS, ANTIGRAVITY_SEARCH_MODELS } = await import("./integrations/antigravity/antigravity");
|
|
243
|
+
return [...ANTIGRAVITY_MODELS, ...ANTIGRAVITY_SEARCH_MODELS];
|
|
244
|
+
},
|
|
239
245
|
loadAuthentication: oauthCapability(() => import("./integrations/antigravity/antigravity-oauth"), "antigravityOAuthClient", { withRefresher: true }),
|
|
240
246
|
loadQuotaCollector: quotaCapability(() => import("./integrations/antigravity/antigravity-quota"), "fetchAntigravityQuota"),
|
|
241
247
|
loadModelDiscovery: async () => async ({ baseUrl, credential, fetcher }) => {
|