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.
Files changed (126) hide show
  1. package/.env.example +4 -2
  2. package/README.md +25 -1
  3. package/dist/dashboard/assets/{BannedPage-B1N603cM.js → BannedPage-B05aF8Dg.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-9q4_4NoK.js → CliToolDetailPage-CLUhmjUy.js} +2 -2
  6. package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DwshGtK-.js → CliToolsPage-CQmrqqrk.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-D762MIkp.js → CombosPage-CHG_1Wr3.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-KRYjHZsX.js → ConsoleLogPage-DPgbklad.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-d5v3K_Yc.js → InjectSystemPromptPanel-C-e2AMz6.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-Ccv4tjNS.js → LoginPage-J8wPbmfT.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
  21. package/dist/dashboard/assets/{OverviewPage-DN9fYNWA.js → OverviewPage-BSQnF2Lp.js} +1 -1
  22. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +7 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-DxjvUzSD.js → ProxyPage-BBTRBAQ3.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
  31. package/dist/dashboard/assets/{SettingsPage-BW4AKj2H.js → SettingsPage-D7xp5Ij_.js} +1 -1
  32. package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-44BZrxFC.js → SetupPage-LND6_Ss7.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-DRPavCdU.js → StudioPage-BzNnm6Bi.js} +2 -2
  36. package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-B7M7LvMM.js → UsagePage-C1TbL24N.js} +2 -2
  38. package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
  39. package/dist/dashboard/assets/{entry-D6WvojeD.js → entry-vqGb8yVz.js} +3 -3
  40. package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
  41. package/dist/dashboard/assets/formatters-DP-5E01-.js +1 -0
  42. package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
  43. package/dist/dashboard/assets/{index-Ko4dLRnV.js → index-D36vCZht.js} +2 -2
  44. package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
  45. package/dist/dashboard/assets/{select-BHqpd-bg.js → select-C9Ssle9b.js} +1 -1
  46. package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
  47. package/dist/dashboard/index.html +2 -2
  48. package/dist/dashboard/index.html.gz +0 -0
  49. package/dist/dashboard/precompressed-assets.json +22 -22
  50. package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
  51. package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
  52. package/package.json +3 -1
  53. package/src/config/security.ts +1 -1
  54. package/src/config/spec.ts +13 -6
  55. package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
  56. package/src/console/cli-tools/injectors/opencode.ts +42 -14
  57. package/src/console/providers/catalog/contracts.ts +4 -0
  58. package/src/console/providers/catalog/routes.ts +1 -0
  59. package/src/console/providers/detail/routes.ts +8 -0
  60. package/src/console/providers/detail/store.ts +4 -0
  61. package/src/console/quota/account-quota-tenant.ts +74 -0
  62. package/src/observability/telemetry-buffer.ts +2 -1
  63. package/src/persistence/boot.ts +13 -4
  64. package/src/persistence/schema.ts +7 -1
  65. package/src/protocol/canonical-model.ts +7 -0
  66. package/src/protocol/request/gemini.ts +20 -1
  67. package/src/protocol/tool-identity.ts +15 -1
  68. package/src/providers/default-registry.ts +8 -2
  69. package/src/providers/discovery/discovery-types.ts +18 -0
  70. package/src/providers/discovery/probe-phases.ts +45 -0
  71. package/src/providers/discovery/probing-service.ts +41 -6
  72. package/src/providers/integrations/antigravity/antigravity-protocol.ts +20 -2
  73. package/src/providers/integrations/antigravity/antigravity.ts +24 -0
  74. package/src/providers/integrations/codex/codex.ts +28 -1
  75. package/src/providers/integrations/qoder/chat-proto.ts +328 -0
  76. package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
  77. package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
  78. package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
  79. package/src/providers/integrations/qoder/qoder-body.ts +78 -0
  80. package/src/providers/integrations/qoder/qoder-catalog.ts +5 -1
  81. package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
  82. package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
  83. package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
  84. package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
  85. package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
  86. package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
  87. package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
  88. package/src/providers/integrations/qoder/qoder-types.ts +51 -0
  89. package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
  90. package/src/providers/integrations/qoder/qoder.ts +147 -1889
  91. package/src/providers/provider-contracts.ts +7 -0
  92. package/src/providers/provider-usage.ts +9 -6
  93. package/src/security/abuse.ts +5 -5
  94. package/src/transport/dispatch/upstream.ts +4 -2
  95. package/src/transport/routing/route-catalog.ts +8 -0
  96. package/src/transport/routing/route-model.ts +4 -0
  97. package/src/transport/routing/router.ts +8 -117
  98. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  99. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  100. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  101. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  102. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  103. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +0 -1
  104. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  105. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +0 -1
  106. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  107. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  108. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  109. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  110. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +0 -5
  111. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  112. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +0 -1
  113. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  114. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  115. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +0 -1
  116. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  117. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  118. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  119. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  120. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  121. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  122. package/dist/dashboard/assets/formatters-u7JPB5BX.js +0 -1
  123. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  124. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  125. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  126. package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
@@ -74,6 +74,17 @@ export interface ProbeModelRequest {
74
74
  reasoningEffort?: ProbeReasoningEffort;
75
75
  maxOutputTokens?: number;
76
76
  stream?: boolean;
77
+ /**
78
+ * Provider-specific transport override for the probe.
79
+ *
80
+ * `undefined` (default) probes through the same transport live traffic
81
+ * uses. A provider may expose extra surfaces that only a probe can force —
82
+ * Qoder's IDE surface (`/model/v1`) is the only one that echoes strategy
83
+ * metadata (`provider_model`, `target`, …) alongside the answer, while the
84
+ * production transports (gRPC / COSY) do not. Forcing it makes the probe
85
+ * result the richest available source of truth.
86
+ */
87
+ surface?: "default" | "cosy" | "grpc" | "ide";
77
88
  }
78
89
  export interface ProbeModelResult {
79
90
  accountId?: string;
@@ -93,6 +104,13 @@ export interface ProbeModelResult {
93
104
  reportedModel?: string;
94
105
  statusCode?: number;
95
106
  error?: string;
107
+ /**
108
+ * Provider-specific upstream metadata about the model that actually served
109
+ * the turn (native model id, supply target, tier). Filled when the probed
110
+ * transport exposes it — Qoder's IDE surface reports it in `raw_usage`;
111
+ * the production transports do not.
112
+ */
113
+ providerMetadata?: Readonly<Record<string, unknown>>;
96
114
  /** Normalized hits a `websearch` probe returned, so the console can show them. */
97
115
  searchResults?: ReadonlyArray<{
98
116
  readonly title: string;
@@ -150,6 +150,27 @@ export function extractSample(events: readonly CanonicalEvent[]): string | undef
150
150
  return trimmed.length > 0 ? trimmed.slice(0, 200) : undefined;
151
151
  }
152
152
 
153
+ /**
154
+ * Reads the model an adapter reported in its canonical events.
155
+ *
156
+ * The HTTP-only `readReportedModel` inspects the raw response body, so it
157
+ * cannot see the model on non-HTTP transports (Qoder gRPC). Adapters surface
158
+ * the upstream echo as `response_start`/`message_start` with a `model` field;
159
+ * this reads that as a fallback.
160
+ */
161
+ export function modelFromEvents(events: readonly CanonicalEvent[]): string | undefined {
162
+ for (const event of events) {
163
+ if (
164
+ (event.type === "response_start" || event.type === "message_start") &&
165
+ typeof event.model === "string" &&
166
+ event.model.trim().length > 0
167
+ ) {
168
+ return event.model.trim();
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+
153
174
  export function hasMeaningfulOutput(events: readonly CanonicalEvent[]): boolean {
154
175
  return events.some((event) => {
155
176
  if (event.type === "tool_call_delta") return true;
@@ -581,6 +602,30 @@ export async function loadProbePreferences(args: {
581
602
  return { probeReasoning, payloadCapture };
582
603
  }
583
604
 
605
+ /**
606
+ * The probe's default identity prompt (`What model are you...`) works for
607
+ * every Qoder roster row except `cmodel` (virtual id `cantus`): that row's
608
+ * gRPC surface answers the identity question with `406 Upstream error [stream]`
609
+ * from the upstream proxy (verified live 2026-11-04; dmodel/qmodel/smodel all
610
+ * accept it). Returns the neutral replacement prompt when the probe targets
611
+ * that row with no explicit prompt, `undefined` otherwise.
612
+ *
613
+ * The replacement prompt must never *ask* an identity question: the upstream
614
+ * guardrail is content-based and swallows the reply (empty sample) for any
615
+ * phrasing that interrogates identity — even indirect ones (`what would you
616
+ * call yourself`, `which model are you`, `perkenalkan dirimu`). A plain
617
+ * greeting that *asserts* the wrong identity (`halo gpt`) makes the model
618
+ * correct us on its own, and returns a sample 3/3 runs (verified live
619
+ * 2026-11-04). We keep that phrasing — it is 100% consistent and the model
620
+ * names itself ("saya Claude... buatan Anthropic") without us ever asking.
621
+ */
622
+ export function qoderCmodelProbePrompt(providerId: string, modelId: string, prompt: string | undefined): string | undefined {
623
+ if (prompt !== undefined) return undefined;
624
+ if (providerId !== "qoder" && providerId !== "qodercn") return undefined;
625
+ if (modelId !== "cmodel" && modelId !== "cantus") return undefined;
626
+ return "halo gpt";
627
+ }
628
+
584
629
  /**
585
630
  * Phase 5 — build the canonical probe request. The probe prompt must pass every
586
631
  * provider's content-policy review (a bare "hi" trips CodeBuddy's 11140 safety
@@ -42,10 +42,12 @@ import {
42
42
  buildProbeCanonicalRequest,
43
43
  computeProbeVerdict,
44
44
  extractSample,
45
+ modelFromEvents,
45
46
  GROK_407_PROBE_PROMPT,
46
47
  grok407ProbeFailure,
47
48
  hasMeaningfulOutput,
48
49
  loadProbePreferences,
50
+ qoderCmodelProbePrompt,
49
51
  recordProbeHealth,
50
52
  resolveProbeAdapter,
51
53
  resolveProbeTarget,
@@ -186,7 +188,16 @@ export class ProviderProbingService {
186
188
  request: ProbeModelRequest,
187
189
  ): Promise<ProbeModelResult> {
188
190
  const startedAt = Date.now();
189
- const modelId = request.modelId;
191
+ // Qoder's IDE surface is the only transport that echoes provider metadata
192
+ // (`provider_model`, `target`, …) in its raw_usage, so probes for Qoder
193
+ // default to it unless the operator picked a surface explicitly. This
194
+ // covers every entry point (single model, all models, all accounts).
195
+ const normalizedRequest: ProbeModelRequest =
196
+ (providerId === "qoder" || providerId === "qodercn") &&
197
+ (request.surface === undefined || request.surface === "default")
198
+ ? { ...request, surface: "ide" }
199
+ : request;
200
+ const modelId = normalizedRequest.modelId;
190
201
  // One provider snapshot per probe: the contract gate, the account gate, the
191
202
  // custom-adapter fallback, and the wire context all read this same row, so a
192
203
  // concurrent edit cannot make two reads disagree.
@@ -207,10 +218,10 @@ export class ProviderProbingService {
207
218
  db: this.db,
208
219
  tenantId,
209
220
  providerId,
210
- requestedAccountId: request.accountId,
221
+ requestedAccountId: normalizedRequest.accountId,
211
222
  requiresAccount,
212
223
  allowCoolingAccount:
213
- providerId === "grok" && request.prompt === GROK_407_PROBE_PROMPT,
224
+ providerId === "grok" && normalizedRequest.prompt === GROK_407_PROBE_PROMPT,
214
225
  });
215
226
  if (!account.ok) {
216
227
  return { ok: false, latencyMs: Date.now() - startedAt, error: account.error };
@@ -236,9 +247,16 @@ export class ProviderProbingService {
236
247
  wireFamily,
237
248
  request,
238
249
  });
250
+ // Qoder's gRPC surface refuses a probe that asks a model to identify
251
+ // itself (406 from the upstream proxy) for one roster row (`cmodel`,
252
+ // virtual id `cantus`). dmodel/qmodel/smodel accept the identity prompt
253
+ // fine — only cmodel refuses it — so the workaround is scoped to that
254
+ // row, not to every Qoder probe, and never overrides an explicit prompt.
255
+ const qoderCmodelPrompt = qoderCmodelProbePrompt(providerId, modelId, normalizedRequest.prompt);
256
+ const probeRequest = qoderCmodelPrompt === undefined ? normalizedRequest : { ...normalizedRequest, prompt: qoderCmodelPrompt };
239
257
  const canonicalRequest = buildProbeCanonicalRequest({
240
258
  modelId,
241
- request,
259
+ request: probeRequest,
242
260
  probeReasoning,
243
261
  sourceSurface,
244
262
  });
@@ -257,6 +275,11 @@ export class ProviderProbingService {
257
275
  // The model row's own output ceiling, so a probe with a max-output
258
276
  // ceiling (OpenCode free) shapes with the model's limit, not a guess.
259
277
  ...(outputLimit == null ? {} : { output_limit: outputLimit }),
278
+ // Probe-only transport override, when the operator asked for one. Live
279
+ // dispatch never sets this.
280
+ ...(normalizedRequest.surface === undefined || normalizedRequest.surface === "default"
281
+ ? {}
282
+ : { probe_surface: normalizedRequest.surface }),
260
283
  };
261
284
 
262
285
  const probeSignal = AbortSignal.timeout(30_000);
@@ -308,7 +331,7 @@ export class ProviderProbingService {
308
331
  `Provider ${providerId} has no web search transport (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
309
332
  );
310
333
  }
311
- const searchProbeQuery = request.prompt?.trim() || "Cartethyia gateway probe";
334
+ const searchProbeQuery = normalizedRequest.prompt?.trim() || "Cartethyia gateway probe";
312
335
  const response = await adapter.websearch(
313
336
  { model: modelId, query: searchProbeQuery, max_results: 3 },
314
337
  candidate,
@@ -574,7 +597,16 @@ export class ProviderProbingService {
574
597
  }
575
598
 
576
599
  const sample = extractSample(events);
577
- const reportedModel = await probeCapture?.reportedModel?.catch(() => undefined);
600
+ // The HTTP-only `readReportedModel` covers response-body transports; a
601
+ // non-HTTP transport (Qoder gRPC) reports the upstream echo as a
602
+ // `response_start` event instead — fall back to that when the body read
603
+ // came up empty.
604
+ const reportedModel =
605
+ (await probeCapture?.reportedModel?.catch(() => undefined)) ?? modelFromEvents(events);
606
+ // Provider-specific upstream model metadata (Qoder IDE surface raw_usage):
607
+ // carried on the terminal event by the adapter, surfaced verbatim.
608
+ const terminal = events.find((event) => event.type === "terminal");
609
+ const providerMetadata = terminal?.type === "terminal" ? terminal.provider_metadata : undefined;
578
610
  return {
579
611
  ...(accountId ? { accountId } : {}),
580
612
  ok,
@@ -583,6 +615,9 @@ export class ProviderProbingService {
583
615
  ...(upstreamStatusCode === undefined ? {} : { statusCode: upstreamStatusCode }),
584
616
  ...(sample ? { sample } : {}),
585
617
  ...(reportedModel === undefined ? {} : { reportedModel }),
618
+ ...(providerMetadata === undefined || Object.keys(providerMetadata).length === 0
619
+ ? {}
620
+ : { providerMetadata }),
586
621
  ...(searchResults !== undefined && searchResults.length > 0 ? { searchResults } : {}),
587
622
  ...(errorMessage ? { error: errorMessage } : {}),
588
623
  };
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { createHash } from "node:crypto";
19
19
  import { providerBaseUrl } from "../../provider-metadata";
20
- import type { ModelDefinition } from "../../provider-model-definition";
20
+ import { defineModel, type ModelDefinition } from "../../provider-model-definition";
21
21
  import { isRecord } from "../../../protocol/codecs";
22
22
  import { modelsDevCatalog } from "../../discovery/models-dev-catalog";
23
23
  import type { CanonicalRequest, ReasoningIntent } from "../../../protocol/canonical-model";
@@ -485,7 +485,25 @@ export async function discoverAntigravityModels(
485
485
  (await response.json()) as unknown,
486
486
  );
487
487
  if (models === null) continue;
488
- return models;
488
+ // Keep the static websearch row on top of discovered rows: the live
489
+ // catalog does not publish search SKUs, and `/v1/search` needs a
490
+ // `service_kind: "websearch"` row to route.
491
+ return [
492
+ ...models,
493
+ defineModel({
494
+ id: "gemini-2.5-flash",
495
+ providerId: "antigravity",
496
+ serviceKind: "websearch",
497
+ wireFamily: "chat",
498
+ // Empty endpoint — see `ANTIGRAVITY_SEARCH_MODELS`.
499
+ endpoint: "",
500
+ ctx: 1_000_000,
501
+ out: 65_535,
502
+ vision: true,
503
+ reasoning: true,
504
+ toolCall: false,
505
+ }),
506
+ ];
489
507
  } catch {
490
508
  // Best-effort: a failed live catalog must never break static routing.
491
509
  }
@@ -116,6 +116,30 @@ export const ANTIGRAVITY_MODELS: readonly ModelDefinition[] = [
116
116
  antigravityModel("gpt-oss-120b", 131_072, 32_768, true),
117
117
  ];
118
118
 
119
+ /**
120
+ * Web-search rows served by `/v1/search`. Antigravity grounds search through a
121
+ * Gemini chat call with a `web_search` tool (see `websearch`); the cheap flash
122
+ * SKU is the default search model.
123
+ */
124
+ export const ANTIGRAVITY_SEARCH_MODELS: readonly ModelDefinition[] = [
125
+ defineModel({
126
+ id: "gemini-2.5-flash",
127
+ providerId: "antigravity",
128
+ serviceKind: "websearch",
129
+ wireFamily: "chat",
130
+ // Empty endpoint: the adapter resolves the URL from the request stream flag
131
+ // (`GENERATE_PATH`), so the row only needs to exist as a websearch route.
132
+ // An empty value also keeps it distinct from the llm row (the unique key is
133
+ // provider+model+endpoint).
134
+ endpoint: "",
135
+ ctx: 1_000_000,
136
+ out: 65_535,
137
+ vision: true,
138
+ reasoning: true,
139
+ toolCall: false,
140
+ }),
141
+ ];
142
+
119
143
  function normalizeAntigravityGrounding(data: unknown, maxResults: number): readonly WebSearchResult[] {
120
144
  if (!isRecord(data)) return [];
121
145
  const chunks = Array.isArray(data.groundingChunks) ? data.groundingChunks : [];
@@ -145,6 +145,30 @@ export const CODEX_MODELS: readonly ModelDefinition[] = [
145
145
  ];
146
146
  const CODEX_SEARCH_DEFAULT_MODEL = "gpt-5.6-luna";
147
147
 
148
+ /**
149
+ * Web-search rows served by `/v1/search`. Codex grounds search through a
150
+ * Responses call with a `web_search` tool (see `websearch`); the cheap 5.6
151
+ * SKU is the default search model.
152
+ */
153
+ export const CODEX_SEARCH_MODELS: readonly ModelDefinition[] = [
154
+ defineModel({
155
+ id: "gpt-5.6-luna",
156
+ providerId: "codex",
157
+ serviceKind: "websearch",
158
+ wireFamily: "responses",
159
+ // Empty endpoint: the adapter falls back to
160
+ // `/backend-api/codex/responses` when the candidate path is empty (see
161
+ // `dispatch`), and an empty value keeps the row distinct from the llm row
162
+ // in the (provider, model, endpoint) unique key.
163
+ endpoint: "",
164
+ ctx: 1_000_000,
165
+ out: 128_000,
166
+ vision: true,
167
+ reasoning: true,
168
+ toolCall: false,
169
+ }),
170
+ ];
171
+
148
172
  function normalizeCodexSearchCitations(
149
173
  citations: readonly unknown[],
150
174
  answerText: string,
@@ -528,7 +552,10 @@ export async function discoverCodexModels(input: {
528
552
  ),
529
553
  );
530
554
  }
531
- return [...byId.values()].sort((a, b) => a.modelId.localeCompare(b.modelId));
555
+ // Keep the static websearch rows on top of discovered llm rows: the codex
556
+ // registry does not publish search SKUs, and `/v1/search` needs a
557
+ // `service_kind: "websearch"` row to route.
558
+ return [...byId.values(), ...CODEX_SEARCH_MODELS].sort((a, b) => a.modelId.localeCompare(b.modelId));
532
559
  }
533
560
 
534
561
  /** Reads just enough of an untyped body to tell SSE from JSON, then replays it. */
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Generated from `vendor/qoder-proto/chat.proto` (Qoder desktop client v1.1.81,
3
+ * `qoder-worker-runtime.mjs`). Do not edit by hand — regenerate from the
4
+ * proto file via `scripts/scratch/gen-chat-proto.ts`.
5
+ */
6
+ export const CHAT_PROTO_SOURCE = `syntax = "proto3";
7
+
8
+ package model.chat;
9
+
10
+ import "google/protobuf/struct.proto";
11
+ import "google/protobuf/wrappers.proto";
12
+
13
+ option java_multiple_files = true;
14
+ option java_package = "com.qoder.grpc.model.chat";
15
+ option java_outer_classname = "ChatProto";
16
+
17
+ // Reasoning effort level for model reasoning
18
+ enum ReasoningEffort {
19
+ REASONING_EFFORT_UNSPECIFIED = 0;
20
+ REASONING_EFFORT_NONE = 1;
21
+ REASONING_EFFORT_LOW = 2;
22
+ REASONING_EFFORT_MEDIUM = 3;
23
+ REASONING_EFFORT_HIGH = 4;
24
+ REASONING_EFFORT_XHIGH = 5;
25
+ REASONING_EFFORT_MAX = 6;
26
+ }
27
+
28
+ // ─── Request messages ────────────────────────────────────────────────────────
29
+
30
+ // Chat completion request matching OpenAI-compatible API
31
+ message ChatCompletionRequest {
32
+ string model = 1;
33
+ repeated ChatMessage messages = 2;
34
+ google.protobuf.DoubleValue temperature = 3;
35
+ google.protobuf.DoubleValue top_p = 4;
36
+ google.protobuf.Int32Value max_tokens = 5;
37
+ bool stream = 6;
38
+ repeated string stop = 7;
39
+ google.protobuf.DoubleValue presence_penalty = 8;
40
+ google.protobuf.DoubleValue frequency_penalty = 9;
41
+ google.protobuf.Int32Value n = 10;
42
+ google.protobuf.StringValue user = 11;
43
+ google.protobuf.Int32Value seed = 12;
44
+ Reasoning reasoning = 13;
45
+ google.protobuf.BoolValue parallel_tool_calls = 14;
46
+ map<string, double> logit_bias = 15;
47
+ repeated Tool tools = 16;
48
+ google.protobuf.Value tool_choice = 17;
49
+ ResponseFormat response_format = 18;
50
+ repeated string modalities = 19;
51
+ StreamOptions stream_options = 20;
52
+ map<string, ChatPatchList> patches = 21;
53
+ ChatMetadata metadata = 22;
54
+ google.protobuf.Struct extras = 23;
55
+ CustomModelConfig custom_model = 24;
56
+ }
57
+
58
+ // Wrapper for repeated ChatPatch (used in patches map value)
59
+ message ChatPatchList {
60
+ repeated ChatPatch patches = 1;
61
+ }
62
+
63
+ // Chat message in the conversation (request side)
64
+ message ChatMessage {
65
+ string cache_id = 1;
66
+ string role = 2;
67
+ // content can be plain text or a list of multimodal content parts
68
+ oneof content_type {
69
+ string text_content = 3;
70
+ ContentPartList parts_content = 12;
71
+ }
72
+ google.protobuf.StringValue name = 4;
73
+ repeated ToolCall tool_calls = 5;
74
+ google.protobuf.StringValue tool_call_id = 6;
75
+ google.protobuf.StringValue tool_call_call_id = 7;
76
+ google.protobuf.StringValue reasoning_content = 8;
77
+ ReasoningItem reasoning_item = 9;
78
+ google.protobuf.Struct extends = 10;
79
+ }
80
+
81
+ // Wrapper for repeated ContentPart
82
+ message ContentPartList {
83
+ repeated ContentPart parts = 1;
84
+ }
85
+
86
+ // Content part for multimodal messages
87
+ message ContentPart {
88
+ string type = 1;
89
+ google.protobuf.StringValue text = 2;
90
+ ImageUrl image_url = 3;
91
+ InputAudio input_audio = 4;
92
+ CacheControl cache_control = 5;
93
+ }
94
+
95
+ // Cache control configuration for content parts
96
+ message CacheControl {
97
+ string type = 1;
98
+ }
99
+
100
+ // Image URL in message content
101
+ message ImageUrl {
102
+ string url = 1;
103
+ google.protobuf.StringValue detail = 2;
104
+ }
105
+
106
+ // Audio input in message content
107
+ message InputAudio {
108
+ string data = 1;
109
+ string format = 2;
110
+ }
111
+
112
+ // Reasoning item for model series (e.g. gpt-5)
113
+ message ReasoningItem {
114
+ string id = 1;
115
+ google.protobuf.StringValue encrypted_content = 2;
116
+ }
117
+
118
+ // Streaming options
119
+ message StreamOptions {
120
+ google.protobuf.BoolValue include_usage = 1;
121
+ }
122
+
123
+ // Tool available for the model to call
124
+ message Tool {
125
+ string cache_id = 1;
126
+ string type = 2;
127
+ Function function = 3;
128
+ Advisor advisor = 4;
129
+ }
130
+
131
+ // Function definition
132
+ message Function {
133
+ string name = 1;
134
+ google.protobuf.StringValue description = 2;
135
+ google.protobuf.Struct parameters = 3;
136
+ google.protobuf.BoolValue strict = 4;
137
+ }
138
+
139
+ // Advisor definition
140
+ message Advisor {
141
+ string name = 1;
142
+ string model = 2;
143
+ int32 max_uses = 3;
144
+ }
145
+
146
+ // Tool call made by the assistant
147
+ message ToolCall {
148
+ google.protobuf.StringValue id = 1;
149
+ google.protobuf.StringValue call_id = 2;
150
+ google.protobuf.StringValue type = 3;
151
+ FunctionCall function = 4;
152
+ }
153
+
154
+ // Function call details
155
+ message FunctionCall {
156
+ string name = 1;
157
+ google.protobuf.Value arguments = 2;
158
+ }
159
+
160
+ // Reasoning configuration
161
+ message Reasoning {
162
+ ReasoningEffort effort = 1;
163
+ }
164
+
165
+ // Response format specification
166
+ message ResponseFormat {
167
+ string type = 1;
168
+ JsonSchema json_schema = 2;
169
+ }
170
+
171
+ // JSON schema for structured outputs
172
+ message JsonSchema {
173
+ string name = 1;
174
+ google.protobuf.StringValue description = 2;
175
+ google.protobuf.Struct schema = 3;
176
+ google.protobuf.BoolValue strict = 4;
177
+ }
178
+
179
+ // Metadata attached to the chat request
180
+ message ChatMetadata {
181
+ WorkspaceMetadata workspace = 1;
182
+ BusinessMetadata business = 2;
183
+ ContextMetadata context = 3;
184
+ UserMetadata user = 4;
185
+ }
186
+
187
+ // Workspace metadata
188
+ message WorkspaceMetadata {
189
+ int32 file_count = 1;
190
+ repeated string languages = 2;
191
+ string codebase_status = 3;
192
+ string codebase_soft_status = 4;
193
+ string codebase_external_id = 5;
194
+ string data_policy = 6;
195
+ }
196
+
197
+ // Business metadata
198
+ message BusinessMetadata {
199
+ string id = 1;
200
+ string name = 2;
201
+ int64 begin_at = 3;
202
+ string product = 4;
203
+ string version = 5;
204
+ string type = 6;
205
+ string stage = 7;
206
+ string sub_task = 8;
207
+ }
208
+
209
+ // Context metadata for request tracing and client identification
210
+ message ContextMetadata {
211
+ string request_id = 1;
212
+ string session_id = 2;
213
+ string task_id = 3;
214
+ string request_set_id = 4;
215
+ string machine_id = 5;
216
+ string client_type = 6;
217
+ string source_session_id = 7;
218
+ }
219
+
220
+ // User metadata
221
+ message UserMetadata {
222
+ string user_id = 1;
223
+ }
224
+
225
+ // Custom model configuration (bring-your-own-key)
226
+ message CustomModelConfig {
227
+ string name = 1;
228
+ string url = 2;
229
+ string style = 3;
230
+ string provider = 4;
231
+ string type = 5;
232
+ string model = 6;
233
+ google.protobuf.Struct parameters = 7;
234
+ }
235
+
236
+ // Patch to apply to the chat request
237
+ message ChatPatch {
238
+ string cache_id = 1;
239
+ string action = 2;
240
+ google.protobuf.Int32Value position = 3;
241
+ google.protobuf.StringValue content = 4;
242
+ google.protobuf.StringValue name = 5;
243
+ google.protobuf.StringValue rename_to = 6;
244
+ google.protobuf.StringValue description = 7;
245
+ google.protobuf.Struct parameters = 8;
246
+ }
247
+
248
+ // ─── Response messages ───────────────────────────────────────────────────────
249
+
250
+ // Shared response chat message type (used in both streaming delta and
251
+ // non-streaming message fields)
252
+ message ResponseChatMessage {
253
+ string role = 1;
254
+ string content = 2;
255
+ string reasoning_content = 3;
256
+ repeated google.protobuf.Struct tool_calls = 4;
257
+ google.protobuf.Value reasoning_item = 5;
258
+ google.protobuf.Struct extends = 6;
259
+ }
260
+
261
+ // Single choice in the non-streaming response
262
+ message NonStreamChoice {
263
+ int32 index = 1;
264
+ ResponseChatMessage message = 2;
265
+ string finish_reason = 3;
266
+ }
267
+
268
+ // Chat completion response (non-streaming)
269
+ message ChatCompletionResponse {
270
+ int64 code = 1;
271
+ string id = 2;
272
+ string object = 3;
273
+ int64 created = 4;
274
+ string model = 5;
275
+ repeated NonStreamChoice choices = 6;
276
+ Usage usage = 7;
277
+ google.protobuf.Struct extras = 8;
278
+ }
279
+
280
+ // Single choice in the streaming chunk
281
+ message StreamChoice {
282
+ int32 index = 1;
283
+ ResponseChatMessage delta = 2;
284
+ string finish_reason = 3;
285
+ }
286
+
287
+ // Streaming response chunk
288
+ message ChatCompletionChunk {
289
+ int64 code = 1;
290
+ string id = 2;
291
+ string object = 3;
292
+ int64 created = 4;
293
+ string model = 5;
294
+ repeated StreamChoice choices = 6;
295
+ Usage usage = 7;
296
+ google.protobuf.Struct extras = 8;
297
+ }
298
+
299
+ // Token usage statistics
300
+ message Usage {
301
+ int32 prompt_tokens = 1;
302
+ int32 completion_tokens = 2;
303
+ int32 total_tokens = 3;
304
+ PromptTokensDetails prompt_tokens_details = 4;
305
+ CompletionTokensDetails completion_tokens_details = 5;
306
+ }
307
+
308
+ // Prompt token details
309
+ message PromptTokensDetails {
310
+ int32 cached_tokens = 1;
311
+ int32 cacheable_tokens = 2;
312
+ }
313
+
314
+ // Completion token details
315
+ message CompletionTokensDetails {
316
+ int32 reasoning_tokens = 1;
317
+ }
318
+
319
+ // ─── Service definition ──────────────────────────────────────────────────────
320
+
321
+ service ChatService {
322
+ // Unary non-streaming chat completion
323
+ rpc ChatCompletion(ChatCompletionRequest) returns (ChatCompletionResponse);
324
+
325
+ // Server-streaming chat completion; each message is one SSE chunk
326
+ rpc ChatCompletionStream(ChatCompletionRequest) returns (stream ChatCompletionChunk);
327
+ }
328
+ `;