clauderipple 0.8.7 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.8 — 2026-10-07
4
+
5
+ ### Fixed
6
+
7
+ - **Web search works with OpenCode Go's DeepSeek on the small/fast slot.** With that slot on
8
+ DeepSeek V4.1 Flash, V4 Flash or V4 Pro through OpenCode Go, every `WebSearch` was refused
9
+ and the Clients screen marked those models "no web search here". The same plan runs the search on
10
+ its Anthropic endpoint, so the router now sends searches for these models there. Their other
11
+ requests are unchanged.
12
+
3
13
  ## 0.8.7 — 2026-10-07
4
14
 
5
15
  ### Fixed
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "private": true,
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "type": "commonjs"
5
5
  }
@@ -23,7 +23,7 @@ import path from "node:path";
23
23
  import http from "node:http";
24
24
  import { execFile, spawn } from "node:child_process";
25
25
  import { fileURLToPath } from "node:url";
26
- import { homeDir, validate } from "./config.js";
26
+ import { homeDir, searchProviderFor, validate } from "./config.js";
27
27
  import { PRESETS } from "./presets.js";
28
28
  import { resolveCompatibleCaps } from "./compat.js";
29
29
  import { CHATGPT_FALLBACK_MODELS } from "./providers/chatgpt/catalog.js";
@@ -318,6 +318,11 @@ async function buildStatus(deps, opts = {}) {
318
318
  // events — a backend the router does not have, so a search still cannot go through it.
319
319
  // Naming the provider's own capability instead would be a promise the router cannot keep.
320
320
  ...(providerCanSearch(name, cfg) ? { webSearch: true } : {}),
321
+ // A provider whose plan searches on another endpoint does so for some models only.
322
+ ...(() => {
323
+ const models = (("models" in p && p.models) || []).map((m) => typeof m === "string" ? m : m.id).filter((id) => searchProviderFor(p, id));
324
+ return models.length ? { webSearchModels: models } : {};
325
+ })(),
321
326
  // Reaching the host says nothing about being able to use it: a chatgpt provider with no
322
327
  // credentials is not "connected", and calling it that sends the user off believing it works.
323
328
  ...(p.type === "chatgpt" ? { needsLogin: !chatgptSignedIn(p.auth) } : {}),
@@ -6,6 +6,7 @@
6
6
  import fs from "node:fs";
7
7
  import os from "node:os";
8
8
  import path from "node:path";
9
+ import { PRESETS } from "./presets.js";
9
10
  /**
10
11
  * The provider's own key, re-sent under the header another wire on the same account expects.
11
12
  *
@@ -76,6 +77,27 @@ export function providerFor(provider, model) {
76
77
  }
77
78
  return { ...shared, type: "openai-compatible", url: url ?? "", ...(entry.wire ? { wire: entry.wire } : {}) };
78
79
  }
80
+ /**
81
+ * The Anthropic endpoint a web search for `model` runs on, when the provider's preset measured that
82
+ * its plan searches there although the model is spoken to on another wire (`anthropicSearch`).
83
+ * OpenCode Go's DeepSeek models answer on Chat Completions, which has no search, while the same key
84
+ * on the plan's Anthropic endpoint runs the server tool (2026-10-07).
85
+ */
86
+ export function searchProviderFor(provider, model) {
87
+ const presetId = provider && "preset" in provider ? provider.preset : undefined;
88
+ const search = presetId ? PRESETS.find((entry) => entry.id === presetId)?.anthropicSearch : undefined;
89
+ if (!provider || !search || !search.models.includes(model))
90
+ return undefined;
91
+ const headers = reauthorized("headers" in provider ? provider.headers : undefined, search.authHeader);
92
+ const sessionHeader = "sessionHeader" in provider ? provider.sessionHeader : undefined;
93
+ return {
94
+ type: "anthropic-compatible",
95
+ url: search.url,
96
+ ...(headers ? { headers } : {}),
97
+ ...(sessionHeader ? { sessionHeader } : {}),
98
+ ...(presetId ? { preset: presetId } : {}),
99
+ };
100
+ }
79
101
  export const PICKER_HOSTS_DEFAULT = ["claude.ai"];
80
102
  export function terminateHosts(c) {
81
103
  const hosts = [c.upstream];
@@ -260,6 +260,13 @@ export const PRESETS = [
260
260
  // to the Responses models above; the Chat and Anthropic ones carry their own empty ladder.
261
261
  effortLevels: ["none", "minimal", "low", "medium", "high", "xhigh"],
262
262
  thinking: "none",
263
+ // The DeepSeek models are served on Chat Completions, which ignores every search option, but
264
+ // the same key on the Anthropic endpoint runs `web_search_20250305` for them: Claude Code's own
265
+ // side request came back with a `web_search_tool_result` of ten real pages and
266
+ // `web_search_requests: 1` (measured 2026-10-07; a bearer header there answers "Missing API
267
+ // key"). Kimi K3, MiniMax M3 and Qwen3.8 Flash take the tool on that endpoint and never search;
268
+ // GLM and MiMo are not served there at all.
269
+ anthropicSearch: { url: "https://opencode.ai/zen/go", authHeader: "x-api-key", models: ["deepseek-v4.1-flash", "deepseek-v4-flash", "deepseek-v4-pro"] },
263
270
  verified: true,
264
271
  notes: "One subscription and key, three endpoints by wire. Measured 2026-09-18. Responses: Muse Spark answers, effort ladder tops out at xhigh, prompt cache reached 96% on a repeated turn. Chat: glm-5.3, glm-5.3-flash, kimi-k3, kimi-k2.7-code, deepseek-v4.1-flash, deepseek-v4-pro, longcat-2.0 and mimo-v2.5-pro all answered. Anthropic: minimax-m3, qwen3.8-max and qwen3.8-flash answered with the prompt cache at 99% on a repeated turn, while union-alpha, the free row, answered \"Model is unavailable\". The Muse Spark Contributor tier is ~90% cheaper because Meta states those interactions improve its products, and OpenCode refuses the model until the workspace opts in (403 DataPolicyError); it is also limited to some regions.",
265
272
  docsUrl: "https://opencode.ai/docs/go/",
@@ -31,7 +31,7 @@ import { ChatGptAdapter } from "./providers/chatgpt/index.js";
31
31
  import { OpenAiCompatibleAdapter } from "./providers/openai/index.js";
32
32
  import { GoogleAdapter } from "./providers/google/index.js";
33
33
  import { conversationKey } from "./providers/chatgpt/translate.js";
34
- import { providerFor, terminateHosts } from "./config.js";
34
+ import { providerFor, searchProviderFor, terminateHosts } from "./config.js";
35
35
  import { injectPickerModels, isBootstrapPath } from "./picker.js";
36
36
  import { effortLevelsForModel } from "./admin.js";
37
37
  import { ResponseUsageTap } from "./requestlog.js";
@@ -724,7 +724,9 @@ export class Proxy {
724
724
  // search of a `smallFast` slot pointed at Luna while the Clients screen called that provider
725
725
  // searchable (2026-10-07: 41 searches failed with nothing on screen to say why). A configured
726
726
  // backend on this same provider has already had its try above.
727
- const own = cfg.providers[route.provider]?.type === "chatgpt" && cfg.webSearch?.provider !== route.provider;
727
+ // The same holds for a plan that searches on another of its endpoints (`anthropicSearch`).
728
+ const own = (cfg.providers[route.provider]?.type === "chatgpt" || searchProviderFor(cfg.providers[route.provider], route.model) !== undefined) &&
729
+ cfg.webSearch?.provider !== route.provider;
728
730
  if (own && (await this.serveWebSearch(res, json, search, { provider: route.provider, model: route.model }, record, finish)))
729
731
  return;
730
732
  const blocks = webSearchErrorBlocks(search, "unavailable");
@@ -1358,8 +1360,9 @@ export class Proxy {
1358
1360
  return false;
1359
1361
  }
1360
1362
  // Which backend is chosen below depends on the wire the search model speaks, which may not be
1361
- // the provider's own.
1362
- const provider = providerFor(configured, settings.model);
1363
+ // the provider's own — or on the endpoint its plan searches on, which may not be the model's.
1364
+ const searchVia = searchProviderFor(configured, settings.model);
1365
+ const provider = searchVia ?? providerFor(configured, settings.model);
1363
1366
  const url = "url" in provider && typeof provider.url === "string" ? provider.url : undefined;
1364
1367
  if (!url && provider.type !== "chatgpt") {
1365
1368
  this.deps.log.warn(`web search: provider ${settings.provider} has no url; leaving the request alone`);
@@ -1368,10 +1371,12 @@ export class Proxy {
1368
1371
  // Which backend depends on how the provider is spoken to, not on the vendor. An
1369
1372
  // anthropic-compatible one is asked in Anthropic's own shape and hands back the blocks the CLI
1370
1373
  // already parses; an openai-compatible one is asked through its chat web plugin.
1374
+ // OpenCode Go refuses a request without its session header outright (400 MissingSessionID).
1375
+ const sessionHeader = "sessionHeader" in provider ? provider.sessionHeader : undefined;
1371
1376
  const common = {
1372
1377
  name: settings.provider,
1373
1378
  url: url ?? "",
1374
- headers: ("headers" in provider && provider.headers) || {},
1379
+ headers: { ...(("headers" in provider && provider.headers) || {}), ...(sessionHeader ? { [sessionHeader]: crypto.randomUUID() } : {}) },
1375
1380
  model: settings.model,
1376
1381
  ...(settings.maxResults ? { maxResults: settings.maxResults } : {}),
1377
1382
  };
@@ -1380,7 +1385,7 @@ export class Proxy {
1380
1385
  // Refuse before sending rather than after. A provider that cannot run the server tool is sent
1381
1386
  // "perform a web search" with no tool attached, and a model told to search with nothing to
1382
1387
  // search with narrates a tool call instead — an answer shaped like success, holding nothing.
1383
- if (!this.canRunServerTools(settings.provider, this.deps.config())) {
1388
+ if (!searchVia && !this.canRunServerTools(settings.provider, this.deps.config())) {
1384
1389
  this.deps.log.warn(`web search: provider ${settings.provider} does not run server tools; leaving the request alone`);
1385
1390
  return false;
1386
1391
  }
@@ -5,4 +5,4 @@
5
5
  // from the manifests. Until 0.1.1 the router and the CLI each carried their own literal, and both
6
6
  // still said "0.1.0" in the 0.1.1 release: after an update nobody could tell which router was
7
7
  // running, and the tray app had nothing to compare (2026-09-15, reported from a Windows install).
8
- export const VERSION = "0.8.7";
8
+ export const VERSION = "0.8.8";
@@ -296,7 +296,8 @@ export function anthropicServerToolBackend(opts) {
296
296
  // No result blocks means the provider did not run the tool, whatever else it said. A model
297
297
  // told to search with no tool it can reach will narrate one instead — an OpenCode Go DeepSeek
298
298
  // answered this exact request with its own tool-call markup as plain text and zero searches
299
- // (measured 2026-09-18). Treat that as the failure it is.
299
+ // (measured 2026-09-18; that same model on the plan's Anthropic endpoint, with its x-api-key
300
+ // header, does search — 2026-10-07). Treat that as the failure it is.
300
301
  const deduped = dedupeHits(hits).slice(0, opts.maxResults ?? 10);
301
302
  if (deduped.length === 0)
302
303
  throw new Error(`${opts.name} web search: no results returned`);
package/dist/ui/app.js CHANGED
@@ -519,8 +519,12 @@ const MODEL_SLOTS = ["smallFast", "subagent", "main"];
519
519
  // A provider the router measured as unable to search. Only the router can know this — it comes from
520
520
  // the status snapshot, never from the model's name — and the `smallFast` slot is exactly where it
521
521
  // matters, because a search sent to such a model comes back as invented prose or a visible failure.
522
- function providerCannotSearch(name) {
523
- return Boolean(status && status.providers && status.providers[name] && status.providers[name].webSearch !== true);
522
+ // A plan that searches on another of its endpoints does so for the models it names only.
523
+ function providerCannotSearch(name, model) {
524
+ const entry = status && status.providers && status.providers[name];
525
+ if (!entry || entry.webSearch === true) return false;
526
+ const id = String(model || "").replace(/@[^@]*$/, "");
527
+ return !(Array.isArray(entry.webSearchModels) && entry.webSearchModels.includes(id));
524
528
  }
525
529
 
526
530
  function renderModelSlots() {
@@ -535,7 +539,7 @@ function renderModelSlots() {
535
539
  if (chosen[slot] === model.id) option.selected = true;
536
540
  // `smallFast` is what a WebSearch runs on, so an inability to search is fatal there and
537
541
  // merely worth knowing everywhere else.
538
- if (slot === "smallFast" && providerCannotSearch(group.name)) {
542
+ if (slot === "smallFast" && providerCannotSearch(group.name, model.id)) {
539
543
  option.textContent += ` — ${t("slots.noWebSearch")}`;
540
544
  }
541
545
  select.appendChild(option);
@@ -572,7 +576,7 @@ async function saveModelSlots() {
572
576
  // screen was already guilty of once. The label on the option warns before the fact; this catches
573
577
  // a session that already had it selected.
574
578
  const owners = models.smallFast ? providersOffering(next, models.smallFast) : [];
575
- if (owners.length && providerCannotSearch(owners[0])) toast(t("slots.noWebSearchWarn"), true);
579
+ if (owners.length && providerCannotSearch(owners[0], models.smallFast)) toast(t("slots.noWebSearchWarn"), true);
576
580
  else toast(t("slots.slotSaved"));
577
581
  } catch (error) { toast(t("common.saveFailed"), true, error.message); }
578
582
  }
@@ -506,7 +506,16 @@ request went, and a `Pane` holds the request log.
506
506
  runs OpenAI's hosted `web_search` and cites real pages, but its wire is OpenAI-shaped while
507
507
  its hits arrive as Responses events (`web_search_call`, `url_citation`), a backend the router
508
508
  does not have. DeepSeek's own endpoint and the ChatGPT provider run Anthropic's server tool;
509
- OpenCode Go's chat and Anthropic endpoints, and every `openai-compatible` provider, do not.
509
+ OpenCode Go's chat endpoint, and every `openai-compatible` provider, do not.
510
+ - **OpenCode Go's Anthropic endpoint does, for DeepSeek only** (corrected 2026-10-07). The
511
+ line above once said it did not. Re-measured with the key in `x-api-key` (a bearer there
512
+ answers "Missing API key"), Claude Code's own side request to `/zen/go/v1/messages` came back
513
+ with ten real hits and `web_search_requests: 1` for deepseek-v4.1-flash, deepseek-v4-flash
514
+ and deepseek-v4-pro; Kimi K3, MiniMax M3 and Qwen3.8 Flash took the tool and never searched;
515
+ GLM and MiMo are not served there. The preset lists those three as `anthropicSearch`, and a
516
+ search routed to one of them (a `smallFast` slot on OpenCode Go's DeepSeek) is relayed there
517
+ by `anthropicServerToolBackend`, though the model's turns stay on Chat Completions. The
518
+ Clients screen reads `webSearchModels` from `/api/status` to stop calling them unsearchable.
510
519
  - **A model that cannot search does not merely decline — it invents.** Measured 2026-09-21
511
520
  against OpenCode Go's chat endpoint: `plugins: [{id: "web"}]` is accepted and ignored (HTTP
512
521
  200, no `annotations`, cache reporting no lookup), and the model answers a search-shaped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauderipple",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "Use GPT (any ChatGPT plan with Codex), DeepSeek and 400+ other models in the Claude Desktop Code tab and Claude Code while staying signed in to your Claude subscription — no third-party gateway mode. Several ChatGPT accounts that take over when one runs out, in Codex too.",
5
5
  "license": "GPL-3.0-only",
6
6
  "workspaces": [