@askalf/dario 6.3.0 → 6.4.0

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/README.md CHANGED
@@ -269,6 +269,8 @@ The tool doesn't know. The backend doesn't know. dario is the seam.
269
269
 
270
270
  A ChatGPT Plus or Pro plan is served on **all three** of dario's endpoints: any client that speaks `/v1/chat/completions` or `/v1/responses` can use it (Codex CLI, the OpenAI SDKs, the Agents SDK, your scripts), and so can any client that speaks `/v1/messages` (Claude Code, the Anthropic SDKs, agent runtimes). The harness never needs to know which subscription is behind it — and the symmetry holds: Codex CLI runs on a Claude plan the same way.
271
271
 
272
+ An Anthropic-shape client that declares Anthropic's hosted `web_search_20260209` tool gets **real web search on the ChatGPT plan** (since 6.4): the plan's own search runs, and the client sees Anthropic's own blocks — `server_tool_use` with the query, `web_search_tool_result` listing the pages searched, the answer with `web_search_result_location` citations. `allowed_domains` and `user_location` carry over, and so does forcing the search with `tool_choice`; `blocked_domains` and `max_uses` do not.
273
+
272
274
  ```bash
273
275
  dario add altman # prints an authorize URL; paste the redirect URL back
274
276
  dario codex list
@@ -216,9 +216,33 @@ export interface ResponsesFunctionTool {
216
216
  parameters: Record<string, unknown>;
217
217
  strict?: boolean | null;
218
218
  }
219
+ /**
220
+ * The Responses hosted web-search tool (v6.4). `filters.allowed_domains` and
221
+ * `user_location` are the two Anthropic web-search options it can carry;
222
+ * `blocked_domains` and `max_uses` have no equivalent and are dropped.
223
+ */
224
+ export interface ResponsesWebSearchTool {
225
+ type: 'web_search';
226
+ filters?: {
227
+ allowed_domains?: string[];
228
+ };
229
+ user_location?: {
230
+ type: 'approximate';
231
+ city?: string;
232
+ region?: string;
233
+ country?: string;
234
+ timezone?: string;
235
+ };
236
+ search_context_size?: 'low' | 'medium' | 'high';
237
+ }
238
+ export type ResponsesTool = ResponsesFunctionTool | ResponsesWebSearchTool;
219
239
  export type ResponsesToolChoice = 'auto' | 'none' | 'required' | {
220
240
  type: 'function';
221
241
  name: string;
242
+ }
243
+ /** Force the hosted web search (the backend accepts `web_search` here, not a function name). */
244
+ | {
245
+ type: 'web_search';
222
246
  };
223
247
  export interface ResponsesReasoningConfig {
224
248
  /**
@@ -235,7 +259,9 @@ export interface ResponsesRequest {
235
259
  model: string;
236
260
  input: ResponsesInputItem[];
237
261
  instructions?: string;
238
- tools?: ResponsesFunctionTool[];
262
+ tools?: ResponsesTool[];
263
+ /** `web_search_call.action.sources` — the searched URLs, which the Anthropic result block needs. */
264
+ include?: string[];
239
265
  tool_choice?: ResponsesToolChoice;
240
266
  parallel_tool_calls?: boolean;
241
267
  reasoning?: ResponsesReasoningConfig;
@@ -292,7 +318,29 @@ export interface ResponsesReasoningItem {
292
318
  encrypted_content?: string | null;
293
319
  status?: string;
294
320
  }
295
- export type ResponsesOutputItem = ResponsesMessageItem | ResponsesResponseFunctionCall | ResponsesReasoningItem | {
321
+ /**
322
+ * A hosted web-search step as the backend reports it. `action.type` is
323
+ * `search` (with `query` and, when `include: web_search_call.action.sources`
324
+ * was asked for, `sources`), `open_page` (`url`) or `find_in_page`
325
+ * (`url`, `pattern`) — probed 2026-09-12 on the ChatGPT backend.
326
+ */
327
+ export interface ResponsesWebSearchCallItem {
328
+ type: 'web_search_call';
329
+ id?: string;
330
+ status?: string;
331
+ action?: {
332
+ type?: string;
333
+ query?: string;
334
+ queries?: string[];
335
+ url?: string;
336
+ pattern?: string;
337
+ sources?: Array<{
338
+ type?: string;
339
+ url?: string;
340
+ }>;
341
+ };
342
+ }
343
+ export type ResponsesOutputItem = ResponsesMessageItem | ResponsesResponseFunctionCall | ResponsesReasoningItem | ResponsesWebSearchCallItem | {
296
344
  type: string;
297
345
  [key: string]: unknown;
298
346
  };
@@ -429,6 +477,15 @@ export type ResponsesAnthropicStreamEvent = {
429
477
  id: string;
430
478
  name: string;
431
479
  input: Record<string, unknown>;
480
+ } | {
481
+ type: 'server_tool_use';
482
+ id: string;
483
+ name: 'web_search';
484
+ input: Record<string, unknown>;
485
+ } | {
486
+ type: 'web_search_tool_result';
487
+ tool_use_id: string;
488
+ content: AnthropicWebSearchResult[];
432
489
  };
433
490
  } | {
434
491
  type: 'content_block_delta';
@@ -442,6 +499,9 @@ export type ResponsesAnthropicStreamEvent = {
442
499
  } | {
443
500
  type: 'input_json_delta';
444
501
  partial_json: string;
502
+ } | {
503
+ type: 'citations_delta';
504
+ citation: AnthropicWebSearchCitation;
445
505
  };
446
506
  } | {
447
507
  type: 'content_block_stop';
@@ -461,6 +521,22 @@ export type ResponsesAnthropicStreamEvent = {
461
521
  } | {
462
522
  type: 'message_stop';
463
523
  };
524
+ /** One searched page, as Anthropic's `web_search_tool_result` lists them. */
525
+ export interface AnthropicWebSearchResult {
526
+ type: 'web_search_result';
527
+ url: string;
528
+ title: string;
529
+ encrypted_content: string;
530
+ page_age: string | null;
531
+ }
532
+ /** A citation on a text block, as Anthropic's `citations_delta` carries it. */
533
+ export interface AnthropicWebSearchCitation {
534
+ type: 'web_search_result_location';
535
+ url: string;
536
+ title: string;
537
+ cited_text: string;
538
+ encrypted_index: string;
539
+ }
464
540
  /**
465
541
  * One parsed Responses SSE event — a typed superset of the fields this
466
542
  * translator consumes. Every event carries a `type`; the rest are present
@@ -480,6 +556,14 @@ export interface ResponsesStreamEvent {
480
556
  summary_index?: number;
481
557
  /** Text / argument / reasoning fragment on `*.delta` events. */
482
558
  delta?: string;
559
+ /** `output_text.annotation.added`: a url_citation over the item's text. */
560
+ annotation?: {
561
+ type?: string;
562
+ url?: string;
563
+ title?: string;
564
+ start_index?: number;
565
+ end_index?: number;
566
+ };
483
567
  /** error event fields. */
484
568
  code?: string | null;
485
569
  message?: string;
@@ -288,7 +288,7 @@ function translateAssistantBlocks(blocks) {
288
288
  out.push(call);
289
289
  return out;
290
290
  }
291
- function translateToolChoice(choice) {
291
+ function translateToolChoice(choice, webSearchName) {
292
292
  if (!choice || typeof choice !== 'object')
293
293
  return undefined;
294
294
  switch (choice.type) {
@@ -299,7 +299,12 @@ function translateToolChoice(choice) {
299
299
  case 'any':
300
300
  return 'required';
301
301
  case 'tool':
302
- // Responses forced form is FLATTENED: {type:'function', name}.
302
+ // Responses forced form is FLATTENED: {type:'function', name}. Forcing
303
+ // the hosted web search is its own shape — `{type:'function', name:
304
+ // 'web_search'}` names a function the request never declared and the
305
+ // backend 400s ("Tool choice 'function' not found in 'tools'").
306
+ if (typeof choice.name === 'string' && choice.name === webSearchName)
307
+ return { type: 'web_search' };
303
308
  return typeof choice.name === 'string'
304
309
  ? { type: 'function', name: choice.name }
305
310
  : 'required';
@@ -361,11 +366,37 @@ export function anthropicToResponsesRequest(body, targetModel, options = {}) {
361
366
  const instructions = flattenSystem(body.system);
362
367
  if (instructions.length > 0)
363
368
  out.instructions = instructions;
369
+ // The client's name for the hosted web search, once one is declared, so a
370
+ // forced tool_choice on it translates to the hosted-tool choice.
371
+ let webSearchName;
364
372
  if (Array.isArray(body.tools)) {
365
373
  const tools = [];
374
+ let webSearch = false;
366
375
  for (const tool of body.tools) {
367
376
  if (!tool || typeof tool.name !== 'string')
368
377
  continue;
378
+ // Anthropic's hosted web search → the backend's hosted web search. The
379
+ // two options both sides speak travel; the rest is dropped. Asking for
380
+ // `action.sources` is what lets the result block name the pages it
381
+ // searched rather than come back empty.
382
+ const t = tool;
383
+ if (typeof t.type === 'string' && t.type.startsWith('web_search') && !webSearch) {
384
+ webSearch = true;
385
+ webSearchName = tool.name;
386
+ const ws = { type: 'web_search' };
387
+ if (Array.isArray(t.allowed_domains) && t.allowed_domains.length > 0)
388
+ ws.filters = { allowed_domains: t.allowed_domains.filter((d) => typeof d === 'string') };
389
+ const loc = t.user_location;
390
+ if (loc && typeof loc === 'object') {
391
+ const l = { type: 'approximate' };
392
+ for (const k of ['city', 'region', 'country', 'timezone'])
393
+ if (typeof loc[k] === 'string')
394
+ l[k] = loc[k];
395
+ ws.user_location = l;
396
+ }
397
+ tools.push(ws);
398
+ continue;
399
+ }
369
400
  if (!tool.input_schema || typeof tool.input_schema !== 'object')
370
401
  continue;
371
402
  const fn = {
@@ -379,8 +410,10 @@ export function anthropicToResponsesRequest(body, targetModel, options = {}) {
379
410
  }
380
411
  if (tools.length > 0)
381
412
  out.tools = tools;
413
+ if (webSearch)
414
+ out.include = ['web_search_call.action.sources'];
382
415
  }
383
- const toolChoice = translateToolChoice(body.tool_choice);
416
+ const toolChoice = translateToolChoice(body.tool_choice, webSearchName);
384
417
  if (toolChoice !== undefined && out.tools)
385
418
  out.tool_choice = toolChoice;
386
419
  if (body.tool_choice?.disable_parallel_tool_use === true && out.tools) {
@@ -579,6 +612,10 @@ export function responsesStreamToAnthropicSSE(options = {}) {
579
612
  const blockByOutputIndex = new Map();
580
613
  let sawToolCall = false;
581
614
  let syntheticToolSeq = 0;
615
+ /** Text streamed per message item, so a url_citation's indices can be turned into cited_text. */
616
+ const textByOutputIndex = new Map();
617
+ /** server_tool_use ids per web_search_call output index, for the result block. */
618
+ const searchIdByOutputIndex = new Map();
582
619
  function ensureStarted(event, events) {
583
620
  if (started)
584
621
  return;
@@ -620,6 +657,46 @@ export function responsesStreamToAnthropicSSE(options = {}) {
620
657
  });
621
658
  return index;
622
659
  }
660
+ /**
661
+ * A hosted web-search step: a `server_tool_use` block opened at
662
+ * output_item.added, its input (query or url) written at output_item.done
663
+ * when the backend reveals the action, then a `web_search_tool_result`
664
+ * block listing the pages the step touched — the searched sources when the
665
+ * request asked for them, the opened page otherwise.
666
+ */
667
+ function openSearchBlock(item, outputIndex, events) {
668
+ const index = nextIndex++;
669
+ open = { kind: 'search', index, outputIndex };
670
+ blockByOutputIndex.set(outputIndex, { kind: 'search', index });
671
+ const id = strOr(item?.id) || `srvtoolu_responses_${syntheticToolSeq++}`;
672
+ searchIdByOutputIndex.set(outputIndex, id);
673
+ events.push({ type: 'content_block_start', index, content_block: { type: 'server_tool_use', id, name: 'web_search', input: {} } });
674
+ return index;
675
+ }
676
+ function finishSearchBlock(item, outputIndex, events) {
677
+ const ws = (item ?? {});
678
+ const action = ws.action ?? {};
679
+ const input = action.type === 'search' || action.query
680
+ ? { query: strOr(action.query) || (Array.isArray(action.queries) ? strOr(action.queries[0]) : '') }
681
+ : action.url ? { url: strOr(action.url), ...(action.pattern ? { pattern: strOr(action.pattern) } : {}) } : {};
682
+ const blk = blockByOutputIndex.get(outputIndex);
683
+ if (blk && blk.kind === 'search') {
684
+ events.push({ type: 'content_block_delta', index: blk.index, delta: { type: 'input_json_delta', partial_json: JSON.stringify(input) } });
685
+ }
686
+ closeOpenBlock(events);
687
+ const results = [];
688
+ const seen = new Set();
689
+ const urls = Array.isArray(action.sources) ? action.sources.map((s) => strOr(s?.url)).filter(Boolean) : action.url ? [strOr(action.url)] : [];
690
+ for (const url of urls) {
691
+ if (seen.has(url))
692
+ continue;
693
+ seen.add(url);
694
+ results.push({ type: 'web_search_result', url, title: '', encrypted_content: '', page_age: null });
695
+ }
696
+ const index = nextIndex++;
697
+ events.push({ type: 'content_block_start', index, content_block: { type: 'web_search_tool_result', tool_use_id: searchIdByOutputIndex.get(outputIndex) ?? '', content: results } });
698
+ events.push({ type: 'content_block_stop', index });
699
+ }
623
700
  function openThinkingBlock(outputIndex, events) {
624
701
  const index = nextIndex++;
625
702
  open = { kind: 'thinking', index, outputIndex };
@@ -719,12 +796,16 @@ export function responsesStreamToAnthropicSSE(options = {}) {
719
796
  openToolBlock(event.item, outputIndex, events);
720
797
  else if (itemType === 'reasoning')
721
798
  openThinkingBlock(outputIndex, events);
799
+ else if (itemType === 'web_search_call')
800
+ openSearchBlock(event.item, outputIndex, events);
722
801
  // message → nothing; the text block opens on the first delta.
723
802
  break;
724
803
  }
725
804
  case 'response.output_text.delta': {
726
805
  if (typeof event.delta === 'string' && event.delta.length > 0) {
727
- const index = ensureTextBlock(numOr(event.output_index, 0), events);
806
+ const oi = numOr(event.output_index, 0);
807
+ const index = ensureTextBlock(oi, events);
808
+ textByOutputIndex.set(oi, (textByOutputIndex.get(oi) ?? '') + event.delta);
728
809
  events.push({
729
810
  type: 'content_block_delta',
730
811
  index,
@@ -733,6 +814,24 @@ export function responsesStreamToAnthropicSSE(options = {}) {
733
814
  }
734
815
  break;
735
816
  }
817
+ case 'response.output_text.annotation.added': {
818
+ // A url_citation over the message text → a citation on the text
819
+ // block, with the cited span cut from what has streamed so far.
820
+ const a = event.annotation;
821
+ if (a && a.type === 'url_citation' && typeof a.url === 'string') {
822
+ const oi = numOr(event.output_index, 0);
823
+ const index = ensureTextBlock(oi, events);
824
+ const text = textByOutputIndex.get(oi) ?? '';
825
+ const from = numOr(a.start_index, 0);
826
+ const to = numOr(a.end_index, from);
827
+ events.push({
828
+ type: 'content_block_delta',
829
+ index,
830
+ delta: { type: 'citations_delta', citation: { type: 'web_search_result_location', url: a.url, title: strOr(a.title), cited_text: text.slice(Math.max(0, from), Math.max(from, to)), encrypted_index: '' } },
831
+ });
832
+ }
833
+ break;
834
+ }
736
835
  case 'response.function_call_arguments.delta': {
737
836
  if (typeof event.delta === 'string' && event.delta.length > 0) {
738
837
  const index = resolveBlock(numOr(event.output_index, 0), 'tool', events);
@@ -758,6 +857,11 @@ export function responsesStreamToAnthropicSSE(options = {}) {
758
857
  }
759
858
  case 'response.output_item.done': {
760
859
  const outputIndex = numOr(event.output_index, 0);
860
+ const itemType = event.item && typeof event.item === 'object' ? event.item.type : undefined;
861
+ if (itemType === 'web_search_call') {
862
+ finishSearchBlock(event.item, outputIndex, events);
863
+ break;
864
+ }
761
865
  if (open && open.outputIndex === outputIndex)
762
866
  closeOpenBlock(events);
763
867
  break;
@@ -861,7 +965,7 @@ export function createAnthropicMessageAssembler() {
861
965
  }
862
966
  else if (e.type === 'content_block_start') {
863
967
  blocks[e.index] = { ...e.content_block };
864
- if (e.content_block.type === 'tool_use')
968
+ if (e.content_block.type === 'tool_use' || e.content_block.type === 'server_tool_use')
865
969
  partialJson.set(e.index, '');
866
970
  }
867
971
  else if (e.type === 'content_block_delta') {
@@ -873,6 +977,8 @@ export function createAnthropicMessageAssembler() {
873
977
  b.thinking = String(b.thinking ?? '') + d.thinking;
874
978
  else if (d.type === 'input_json_delta')
875
979
  partialJson.set(e.index, (partialJson.get(e.index) ?? '') + d.partial_json);
980
+ else if (d.type === 'citations_delta')
981
+ (b.citations = (Array.isArray(b.citations) ? b.citations : [])).push(d.citation);
876
982
  }
877
983
  else if (e.type === 'message_delta') {
878
984
  if (msg) {
@@ -894,7 +1000,7 @@ export function createAnthropicMessageAssembler() {
894
1000
  message(fallbackModel) {
895
1001
  for (const [i, raw] of partialJson) {
896
1002
  const b = blocks[i];
897
- if (!b || b.type !== 'tool_use')
1003
+ if (!b || (b.type !== 'tool_use' && b.type !== 'server_tool_use'))
898
1004
  continue;
899
1005
  b.input = safeParseArguments(raw);
900
1006
  }
@@ -717,6 +717,9 @@ export function createResponsesTranslator(model) {
717
717
  export const CODEX_SUPPORTED_FIELDS = [
718
718
  'model', 'input', 'stream', 'store', 'instructions',
719
719
  'tools', 'tool_choice', 'parallel_tool_calls', 'reasoning',
720
+ // `web_search_call.action.sources` (v6.4): accepted by the backend, probed
721
+ // 2026-09-12 — the searched URLs the Anthropic result block lists.
722
+ 'include',
720
723
  // Sent by the Codex CLI on every request (codex-rs ResponsesApiRequest), so
721
724
  // accepted by construction; see codexPromptCacheKey.
722
725
  'prompt_cache_key',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
5
5
  "type": "module",
6
6
  "bin": {