okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petauth: pass unencoded credential map in oauth exchange3b0866f

main
29.1 KiB995 linesraw
1// Signed Claude SSE replay, schema unions, billing hashes, and stored-credential enumeration have edge cases absent from the fake E2E provider.
2import albedo/daemon/message_content as events
3import albedo/daemon/projection
4import albedo/daemon/transcript
5import albedo/harness/extensions/claude/schema as schemas
6import albedo/harness/extensions/claude/stream
7import albedo/harness/extensions/claude/wire
8import albedo/harness/loop
9import albedo/openai_api
10import albedo/openai_api/request
11import albedo/openai_api/stream as reducer
12import albedo/openai_api/transport
13import albedo/openai_api/types
14import gleam/bit_array
15import gleam/dict
16import gleam/dynamic.{type Dynamic}
17import gleam/dynamic/decode
18import gleam/json
19import gleam/list
20import gleam/option.{None, Some}
21import gleam/result
22import gleam/string
23import gleam/string_tree.{type StringTree}
24
25// 2x3 PNG header, enough for dimension decoding.
26const png = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAAD"
27
28const no_files_home = "/nonexistent"
29
30const subscription =
31  wire.Subscription(
32    "token",
33    "11111111-2222-4333-8444-555555555555",
34    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
35    "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
36  )
37
38pub fn claude_tool_schemas_flatten_only_top_level_combiners_test() -> Result(
39  Dynamic,
40  List(decode.DecodeError),
41) {
42  let assert Ok(schema) =
43    json.parse(
44      "{
45  \"type\": \"object\",
46  \"properties\": {
47    \"urls\": {
48      \"type\": \"array\",
49      \"items\": {
50        \"type\": \"string\"
51      }
52    },
53    \"ids\": {
54      \"type\": \"array\",
55      \"items\": {
56        \"type\": \"string\"
57      }
58    }
59  },
60  \"oneOf\": [
61    {
62      \"required\": [
63        \"urls\"
64      ]
65    },
66    {
67      \"required\": [
68        \"ids\"
69      ]
70    }
71  ],
72  \"allOf\": [
73    {
74      \"required\": [
75        \"mode\"
76      ],
77      \"properties\": {
78        \"mode\": {
79          \"oneOf\": [
80            {
81              \"type\": \"string\"
82            },
83            {
84              \"type\": \"integer\"
85            }
86          ]
87        }
88      }
89    }
90  ]
91}",
92      decode.dynamic,
93    )
94  let request =
95    types.Request(
96      "claude-opus-5-5",
97      None,
98      [types.User("hi")],
99      [
100        types.Tool(
101          "contents",
102          "Fetch URLs or IDs",
103          types.encode_value(schema),
104          False,
105        ),
106      ],
107      None,
108      types.defaults,
109    )
110  let assert Ok(openai_api.Exchange(body: body, ..)) =
111    wire.encode(no_files_home, subscription, request)
112  let assert Ok(value) = json.parse(sent(body), decode.dynamic)
113  let assert Ok([tool]) =
114    decode.run(value, decode.at(["tools"], decode.list(decode.dynamic)))
115  let assert Ok("object") =
116    decode.run(tool, decode.at(["input_schema", "type"], decode.string))
117  let assert Ok(props) =
118    decode.run(
119      tool,
120      decode.at(
121        ["input_schema", "properties"],
122        decode.dict(decode.string, decode.dynamic),
123      ),
124    )
125  assert list.length(dict.to_list(props)) == 3
126  let assert Ok(["mode"]) =
127    decode.run(
128      tool,
129      decode.at(["input_schema", "required"], decode.list(decode.string)),
130    )
131  let assert Ok(hint) =
132    decode.run(tool, decode.at(["input_schema", "description"], decode.string))
133  assert string.contains(hint, "urls or ids")
134  let assert Ok([_, _]) =
135    decode.run(
136      tool,
137      decode.at(
138        ["input_schema", "properties", "mode", "oneOf"],
139        decode.list(decode.dynamic),
140      ),
141    )
142  let assert Error(_) =
143    decode.run(tool, decode.at(["input_schema", "oneOf"], decode.dynamic))
144  let assert Error(_) =
145    decode.run(tool, decode.at(["input_schema", "allOf"], decode.dynamic))
146}
147
148pub fn claude_tool_schema_accepts_branch_only_root_union_test() -> Nil {
149  let assert Ok(schema) =
150    json.parse(
151      "{
152  \"oneOf\": [
153    {
154      \"type\": \"object\",
155      \"properties\": {
156        \"urls\": {
157          \"type\": \"array\"
158        }
159      },
160      \"required\": [
161        \"urls\"
162      ]
163    },
164    {
165      \"type\": \"object\",
166      \"properties\": {
167        \"ids\": {
168          \"type\": \"array\"
169        }
170      },
171      \"required\": [
172        \"ids\"
173      ]
174    }
175  ]
176}",
177      decode.dynamic,
178    )
179  let normalized = schemas.normalize(types.encode_value(schema))
180  let assert Ok(value) = json.parse(json.to_string(normalized), decode.dynamic)
181  let assert Ok(props) =
182    decode.run(
183      value,
184      decode.at(["properties"], decode.dict(decode.string, decode.dynamic)),
185    )
186  assert list.length(dict.to_list(props)) == 2
187  let assert Ok("object") =
188    decode.run(value, decode.at(["type"], decode.string))
189  let assert Ok(hint) =
190    decode.run(value, decode.at(["description"], decode.string))
191  assert string.contains(hint, "urls or ids")
192}
193
194pub fn claude_schema_preserves_metadata_and_merge_precedence_in_requests_test() -> Nil {
195  let assert Ok(schema) =
196    json.parse(
197      "{
198  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",
199  \"$defs\": {
200    \"choice\": {
201      \"anyOf\": [
202        {
203          \"type\": \"string\"
204        },
205        {
206          \"type\": \"null\"
207        }
208      ]
209    }
210  },
211  \"x-metadata\": {
212    \"values\": [
213      null,
214      true,
215      2.5,
216      {
217        \"nested\": [
218          \"keep\"
219        ]
220      }
221    ]
222  },
223  \"additionalProperties\": false,
224  \"description\": \"Choose inputs\",
225  \"properties\": {
226    \"shared\": {
227      \"$ref\": \"#/$defs/choice\",
228      \"x-custom\": [
229        false,
230        null
231      ]
232    }
233  },
234  \"required\": [
235    \"root\",
236    \"root\"
237  ],
238  \"allOf\": [
239    {
240      \"properties\": {
241        \"shared\": {
242          \"type\": \"integer\"
243        },
244        \"mode\": {
245          \"oneOf\": [
246            {
247              \"const\": \"a\"
248            },
249            {
250              \"const\": \"b\"
251            }
252          ]
253        }
254      },
255      \"required\": [
256        \"mode\"
257      ]
258    }
259  ],
260  \"oneOf\": [
261    {
262      \"properties\": {
263        \"left\": {
264          \"type\": \"string\"
265        },
266        \"collision\": {
267          \"const\": \"first\"
268        }
269      },
270      \"required\": [
271        \"common\",
272        \"left\"
273      ]
274    },
275    {
276      \"properties\": {
277        \"right\": {
278          \"type\": \"string\"
279        },
280        \"collision\": {
281          \"const\": \"second\"
282        }
283      },
284      \"required\": [
285        \"right\",
286        \"common\"
287      ]
288    }
289  ],
290  \"anyOf\": [
291    {
292      \"properties\": {
293        \"extra\": {
294          \"type\": \"boolean\"
295        }
296      },
297      \"required\": [
298        \"common\",
299        \"extra\"
300      ]
301    },
302    {
303      \"properties\": {
304        \"other\": false
305      },
306      \"required\": [
307        \"other\",
308        \"common\"
309      ]
310    }
311  ]
312}",
313      decode.dynamic,
314    )
315  let request =
316    types.Request(
317      "claude-opus-5-5",
318      None,
319      [types.User("hi")],
320      [types.Tool("inputs", "Choose inputs", types.encode_value(schema), False)],
321      None,
322      types.defaults,
323    )
324  let assert Ok(openai_api.Exchange(body: body, ..)) =
325    wire.encode(no_files_home, subscription, request)
326  let assert Ok(value) = json.parse(sent(body), decode.dynamic)
327  let assert Ok([tool]) =
328    decode.run(value, decode.at(["tools"], decode.list(decode.dynamic)))
329  let assert Ok(actual) =
330    decode.run(tool, decode.at(["input_schema"], decode.dynamic))
331  let assert Ok(expected) =
332    json.parse(
333      "{
334  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",
335  \"$defs\": {
336    \"choice\": {
337      \"anyOf\": [
338        {
339          \"type\": \"string\"
340        },
341        {
342          \"type\": \"null\"
343        }
344      ]
345    }
346  },
347  \"x-metadata\": {
348    \"values\": [
349      null,
350      true,
351      2.5,
352      {
353        \"nested\": [
354          \"keep\"
355        ]
356      }
357    ]
358  },
359  \"additionalProperties\": false,
360  \"description\": \"Choose inputs; Exactly one of: common + left or right + common; At least one of: common + extra or other + common\",
361  \"properties\": {
362    \"shared\": {
363      \"$ref\": \"#/$defs/choice\",
364      \"x-custom\": [
365        false,
366        null
367      ]
368    },
369    \"mode\": {
370      \"oneOf\": [
371        {
372          \"const\": \"a\"
373        },
374        {
375          \"const\": \"b\"
376        }
377      ]
378    },
379    \"left\": {
380      \"type\": \"string\"
381    },
382    \"collision\": {
383      \"const\": \"first\"
384    },
385    \"right\": {
386      \"type\": \"string\"
387    },
388    \"extra\": {
389      \"type\": \"boolean\"
390    },
391    \"other\": false
392  },
393  \"required\": [
394    \"common\",
395    \"mode\",
396    \"root\"
397  ],
398  \"type\": \"object\"
399}",
400      decode.dynamic,
401    )
402  assert actual == expected
403}
404
405pub fn claude_stream_preserves_tool_calls_and_replay_test() -> Nil {
406  let chunks = [
407    "{\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"usage\":{\"input_tokens\":12,\"output_tokens\":0,\"cache_read_input_tokens\":200,\"cache_creation_input_tokens\":14}}}",
408    "{\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}",
409    "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}",
410    "{\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"tool_1\",\"name\":\"Bash\",\"input\":{}}}",
411    "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"command\\\":\\\"pwd\\\"}\"}}",
412    "{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":7}}",
413    "{\"type\":\"message_stop\"}",
414  ]
415  let reducer =
416    stream.reducer("claude-opus-5-5", [
417      types.Tool("bash", "run", json.object([]), False),
418    ])
419  let assert Ok(turn) = feed_all(reducer, chunks)
420  let assert [call] = turn.tool_calls
421  assert call.name == "bash"
422  assert call.arguments == "{\"command\":\"pwd\"}"
423  assert turn.call_indices == [#("tool_1", 1)]
424  assert turn.finish == types.ToolCalls
425  let assert Some(usage) = turn.usage
426  assert usage.input_tokens == 226
427  assert usage.output_tokens == 7
428  assert usage.cached_input_tokens == Some(200)
429  assert usage.cache_creation_tokens == Some(14)
430  let request =
431    types.Request(
432      "claude-opus-5-5",
433      None,
434      [
435        types.User("start"),
436        types.Replay(list_first(turn.output)),
437        types.ToolOutput("tool_1", "ok", []),
438      ],
439      [],
440      None,
441      types.defaults,
442    )
443  let assert Ok(openai_api.Exchange(body: body, ..)) =
444    wire.encode(no_files_home, subscription, request)
445  let assert Ok(value) = json.parse(sent(body), decode.dynamic)
446  let assert Ok(names) =
447    decode.run(
448      value,
449      decode.at(["messages"], decode.list(decode.at(["role"], decode.string))),
450    )
451  assert names == ["user", "assistant", "user"]
452  let encoded = sent(body)
453  assert string.contains(encoded, "tool_result")
454  assert string.contains(encoded, "tool_use")
455  assert string.contains(encoded, "hello")
456}
457
458pub fn claude_refusal_details_and_text_reach_failure_message_test() -> Nil {
459  let assert Ok(turn) =
460    feed_all(stream.reducer("claude-opus-5-5", []), [
461      "{\"type\":\"message_start\",\"message\":{\"id\":\"msg_refusal\"}}",
462      "{\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}",
463      "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I can’t help with that request.\"}}",
464      "{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"refusal\",\"stop_details\":{\"type\":\"refusal\",\"category\":\"policy\",\"explanation\":\"private credentials\"}}}",
465      "{\"type\":\"message_stop\"}",
466    ])
467  let failure = loop.stopped_message(turn.finish, turn.output)
468  assert string.contains(failure, "refusal (policy): private credentials")
469  assert string.contains(failure, "I can’t help with that request.")
470}
471
472pub fn claude_stream_shows_thinking_and_replays_it_signed_test() -> Nil {
473  let chunks = [
474    "{\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"usage\":{\"input_tokens\":3,\"output_tokens\":0}}}",
475    "{\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}",
476    "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"weighing \"}}",
477    "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"options\"}}",
478    "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"sig_1\"}}",
479    "{\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}",
480    "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"done\"}}",
481    "{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"}}",
482    "{\"type\":\"message_stop\"}",
483  ]
484  let #(deltas, turn) =
485    feed_thinking(stream.reducer("claude-opus-5-5", []), chunks, [])
486  assert deltas == ["weighing ", "options"]
487  let item = list_first(turn.output)
488  assert events.thinking_text(item) == "weighing options"
489  let request =
490    types.Request(
491      "claude-opus-5-5",
492      None,
493      [types.User("start"), types.Replay(item), types.User("next")],
494      [],
495      None,
496      types.defaults,
497    )
498  let assert Ok(openai_api.Exchange(body: body, ..)) =
499    wire.encode(no_files_home, subscription, request)
500  let assert Ok(value) = json.parse(sent(body), decode.dynamic)
501  let block = fn(name) {
502    decode.at(
503      ["messages"],
504      decode.list(decode.at(
505        ["content"],
506        decode.list(decode.optional_field(
507          name,
508          "",
509          decode.string,
510          decode.success,
511        )),
512      )),
513    )
514  }
515  let assert Ok([_, [thinking, _], _]) = decode.run(value, block("thinking"))
516  assert thinking == "weighing options"
517  let assert Ok([_, [signature, _], _]) = decode.run(value, block("signature"))
518  assert signature == "sig_1"
519
520  let entry =
521    transcript.Entry(types.Replay(item), None, Some("claude"), None, None)
522  let assert Ok([types.Assistant("done"), types.Assistant(summary)]) =
523    projection.for_model([entry], "codex", types.Responses)
524  assert summary == "[Reasoning summary]\nweighing options"
525  let assert Ok(codex_body) =
526    request.encode(
527      types.Responses,
528      types.Request(
529        "gpt-5",
530        None,
531        [types.Assistant(summary)],
532        [],
533        None,
534        types.defaults,
535      ),
536    )
537  assert string.contains(sent(codex_body), "weighing options")
538
539  let changed =
540    types.Request(
541      "claude-sonnet-4-6",
542      None,
543      [types.Replay(item)],
544      [],
545      None,
546      types.defaults,
547    )
548  let assert Ok(openai_api.Exchange(body: switched, ..)) =
549    wire.encode(no_files_home, subscription, changed)
550  assert string.contains(sent(switched), "[Reasoning summary]")
551  assert !string.contains(sent(switched), "sig_1")
552}
553
554pub fn codex_reasoning_summary_survives_transfer_to_claude_test() -> Nil {
555  let assert Ok(item) =
556    json.parse(
557      "{\"type\":\"reasoning\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"first\"},{\"type\":\"summary_text\",\"text\":\"second\"}],\"encrypted_content\":\"sealed-reasoning\"}",
558      types.replay_decoder(types.Responses),
559    )
560  let entry =
561    transcript.Entry(types.Replay(item), None, Some("codex"), None, None)
562  let assert Ok([types.Assistant(summary)]) =
563    projection.for_model([entry], "other-codex", types.Responses)
564  assert summary == "[Reasoning summary]\nfirst\n\nsecond"
565  let assert Ok([types.Replay(projected)]) =
566    projection.for_model([entry], "claude", types.ChatCompletions)
567  let request =
568    types.Request(
569      "claude-opus-5-5",
570      None,
571      [types.Replay(projected)],
572      [],
573      None,
574      types.defaults,
575    )
576  let assert Ok(openai_api.Exchange(body: body, ..)) =
577    wire.encode(no_files_home, subscription, request)
578  let sent = sent(body)
579  assert string.contains(sent, "[Reasoning summary]")
580  assert string.contains(sent, "first")
581  assert string.contains(sent, "second")
582  assert !string.contains(sent, "sealed-reasoning")
583}
584
585/// Feeds every chunk and keeps the thinking deltas the stream emitted.
586fn feed_thinking(
587  reducer: reducer.Reducer,
588  chunks: List(String),
589  deltas: List(String),
590) -> #(List(String), types.Turn) {
591  let assert [chunk, ..rest] = chunks
592  let assert Ok(#(next, emitted, turn)) = reducer.feed(chunk)
593  let deltas =
594    list.fold(emitted, deltas, fn(deltas, event) {
595      case event {
596        types.ThinkingDelta(text) -> [text, ..deltas]
597        _ -> deltas
598      }
599    })
600  case turn {
601    Some(turn) -> #(list.reverse(deltas), turn)
602    None -> feed_thinking(next, rest, deltas)
603  }
604}
605
606pub fn claude_billing_hash_agrees_across_chunk_boundaries_test() -> Nil {
607  let body =
608    string.repeat(
609      "{\"messages\":[{\"role\":\"user\",\"content\":\"hello cch=00000\"}],",
610      37,
611    )
612  assert billing_hash(body) == billing_hash_streamed([body])
613  assert billing_hash(body) == billing_hash_streamed(split_every(body, 1))
614  assert billing_hash(body) == billing_hash_streamed(split_every(body, 7))
615  assert billing_hash(body) == billing_hash_streamed(split_every(body, 13))
616  assert billing_hash(body) == billing_hash_streamed(split_every(body, 31))
617  assert billing_hash(body) == billing_hash_streamed(split_every(body, 32))
618  assert billing_hash(body) == billing_hash_streamed(split_every(body, 33))
619  assert billing_hash(body)
620    == billing_hash_streamed([
621      string.slice(body, 0, 31),
622      string.drop_start(body, 31),
623    ])
624  assert billing_hash(body)
625    == billing_hash_streamed([
626      string.slice(body, 0, 32),
627      string.drop_start(body, 32),
628    ])
629}
630
631fn split_every(text: String, size: Int) -> List(String) {
632  case string.byte_size(text) <= size {
633    True -> [text]
634    False -> [
635      string.slice(text, 0, size),
636      ..split_every(string.drop_start(text, size), size)
637    ]
638  }
639}
640
641type Fixture
642
643type ServerMode {
644  Stream
645}
646
647@external(erlang, "albedo_openai_transport_test_server", "start")
648fn start(mode: ServerMode) -> Fixture
649
650@external(erlang, "albedo_openai_transport_test_server", "url")
651fn url(fixture: Fixture) -> String
652
653@external(erlang, "albedo_openai_transport_test_server", "await_body")
654fn await_body(fixture: Fixture) -> BitArray
655
656@external(erlang, "albedo_openai_transport_test_server", "stop")
657fn stop(fixture: Fixture) -> Nil
658
659pub fn claude_attested_body_streams_through_the_transport_test() -> Nil {
660  let fixture = start(Stream)
661  let image =
662    stored_image(
663      "image/png",
664      string.repeat("ab", 32),
665      string.byte_size(png),
666      fn() { Ok(png) },
667      2,
668      3,
669      24,
670    )
671  let request =
672    types.Request(
673      "claude-opus-5-5",
674      None,
675      [types.UserImage("what is this", [image])],
676      [],
677      None,
678      types.defaults,
679    )
680  let assert Ok(openai_api.Exchange(body: body, ..)) =
681    wire.encode(no_files_home, subscription, request)
682  let assert Ok(connection) = transport.open(url(fixture), [], body, 2000)
683  let received = await_body(fixture)
684  transport.close(connection)
685  stop(fixture)
686  let assert Ok(on_the_wire) = bit_array.to_string(received)
687  let assert Ok(materialized) = materialize(body)
688  assert on_the_wire == materialized
689  assert string.contains(on_the_wire, png)
690  let assert Ok(value) = json.parse(on_the_wire, decode.dynamic)
691  let assert Ok([billing, _]) =
692    decode.run(
693      value,
694      decode.at(["system"], decode.list(decode.at(["text"], decode.string))),
695    )
696  let assert [_, hash_with_end] = string.split(billing, "cch=")
697  let assert [hash, ""] = string.split(hash_with_end, ";")
698  let unsigned = string.replace(on_the_wire, "cch=" <> hash, "cch=00000")
699  assert hash == billing_hash(unsigned)
700}
701
702pub fn claude_first_user_surrogates_do_not_crash_the_billing_sample_test() -> Nil {
703  let request =
704    types.Request(
705      "claude-opus-5-5",
706      None,
707      [types.User("abcd🚀 let's look at 🌟 and 🎉 together")],
708      [],
709      None,
710      types.defaults,
711    )
712  let assert Ok(exchange) = wire.encode(no_files_home, subscription, request)
713  let text = sent(exchange.body)
714  let assert Ok(value) = json.parse(text, decode.dynamic)
715  let assert Ok([billing, _]) =
716    decode.run(
717      value,
718      decode.at(["system"], decode.list(decode.at(["text"], decode.string))),
719    )
720  let assert [_, hash_with_end] = string.split(billing, "cch=")
721  let assert [hash, ""] = string.split(hash_with_end, ";")
722  let unsigned = string.replace(text, "cch=" <> hash, "cch=00000")
723  assert hash == billing_hash(unsigned)
724}
725
726pub fn claude_token_response_becomes_the_stored_credential_test() -> Nil {
727  let assert Ok(value) =
728    claude_token(
729      "{\"access_token\":\"tok\",\"refresh_token\":\"r\",\"expires_in\":86400}",
730    )
731  let assert Ok("oauth") = decode.run(value, decode.at(["type"], decode.string))
732  let assert Ok("tok") = decode.run(value, decode.at(["access"], decode.string))
733  let assert Ok(expires) = decode.run(value, decode.at(["expires"], decode.int))
734  assert expires > 0
735  let assert Ok(account) =
736    decode.run(value, decode.at(["accountId"], decode.string))
737  assert string.byte_size(account) == 16
738}
739
740pub fn claude_billing_hash_matches_reference_vectors_test() -> Nil {
741  assert billing_hash("cch=00000") == "a47f7"
742  assert billing_hash("{\"messages\":[],\"cch=00000\",\"x\":1}") == "3073d"
743  assert billing_hash(
744      "x-anthropic-billing-header: cc_version=2.1.158; cc_entrypoint=cli; cch=00000;",
745    )
746    == "f2b0b"
747  assert billing_hash("") == "b46c5"
748  assert billing_hash(string.repeat("a", 31)) == "ef013"
749  assert billing_hash(string.repeat("a", 32)) == "86567"
750  assert billing_hash(string.repeat("a", 33)) == "5bea8"
751  assert billing_hash(string.repeat("b", 63)) == "9f9d8"
752  assert billing_hash(string.repeat("c", 64)) == "2742b"
753  assert billing_hash(string.repeat("d", 65)) == "92299"
754  assert billing_hash(string.repeat("0123456789", 100)) == "827e8"
755}
756
757@external(erlang, "albedo_claude_auth", "token")
758fn claude_token(response: String) -> Result(Dynamic, String)
759
760/// The ledger records `wire.cache_marks` as what a Claude request asked to
761/// cache; it must name exactly the cache_control markers `encode` sends.
762pub fn claude_cache_marks_match_the_encoded_markers_test() -> Nil {
763  let requests = [
764    types.Request(
765      "claude-opus-5-5",
766      Some("be brief"),
767      [types.User("a"), types.Assistant("b"), types.User("c")],
768      [
769        types.Tool("read", "read", json.object([]), False),
770        types.Tool("bash", "run", json.object([]), False),
771      ],
772      None,
773      types.defaults,
774    ),
775    types.Request(
776      "claude-opus-5-5",
777      None,
778      [types.User("hi")],
779      [],
780      None,
781      types.defaults,
782    ),
783  ]
784  use request <- list.each(requests)
785  use auth <- list.each([subscription, wire.ApiKey("sk-ant-test")])
786  {
787    let assert Ok(openai_api.Exchange(body: body, ..)) =
788      wire.encode(no_files_home, auth, request)
789    let assert Ok(value) = json.parse(sent(body), decode.dynamic)
790    // A request without tools sends no tools field at all.
791    let markers = fn(field) {
792      let assert Ok(blocks) =
793        decode.run(
794          value,
795          decode.optional_field(
796            field,
797            [],
798            decode.list(marker_decoder()),
799            decode.success,
800          ),
801        )
802      option.values(blocks)
803    }
804    let assert Ok(messages) =
805      decode.run(
806        value,
807        decode.at(
808          ["messages"],
809          decode.list(decode.at(["content"], decode.list(marker_decoder()))),
810        ),
811      )
812    let last_message = list.length(messages) - 1
813    let input =
814      messages
815      |> list.index_map(fn(blocks, at) {
816        let last_block = list.length(blocks) - 1
817        list.index_map(blocks, fn(ttl, block) {
818          case ttl, at == last_message && block == last_block {
819            Some(ttl), True -> [
820              #(types.InputSpan(list.length(request.input) - 1), ttl),
821            ]
822            // A marker anywhere but the final block matches no declared mark.
823            Some(ttl), False -> [#(types.InputSpan(-1), ttl)]
824            None, _ -> []
825          }
826        })
827      })
828      |> list.flatten
829      |> list.flatten
830    let encoded =
831      list.flatten([
832        list.map(markers("tools"), fn(ttl) { #(types.ToolsSpan, ttl) }),
833        list.map(markers("system"), fn(ttl) { #(types.SystemSpan, ttl) }),
834        input,
835      ])
836    let declared =
837      wire.cache_marks(request)
838      |> list.map(fn(mark) { #(mark.through, mark.ttl_seconds) })
839    assert declared == encoded
840  }
841}
842
843/// A block's cache_control TTL in seconds, when it carries one.
844fn marker_decoder() -> decode.Decoder(option.Option(Int)) {
845  let ttl = {
846    use ttl <- decode.optional_field("ttl", "5m", decode.string)
847    decode.success(case ttl {
848      "1h" -> 3600
849      _ -> 300
850    })
851  }
852  decode.optional_field(
853    "cache_control",
854    None,
855    decode.optional(ttl),
856    decode.success,
857  )
858}
859
860/// A Console key bills per token, so it drops the subscription's billing
861/// block, metadata and OAuth shape, but premium models answer a headerless 429
862/// unless the Claude Code identity still leads the system prompt.
863pub fn claude_api_key_requests_keep_only_the_identity_test() -> Result(
864  List(String),
865  List(decode.DecodeError),
866) {
867  let request =
868    types.Request(
869      "claude-opus-5-5",
870      None,
871      [types.User("hi")],
872      [],
873      None,
874      types.defaults,
875    )
876  let assert Ok(exchange) =
877    wire.encode(no_files_home, wire.ApiKey("sk-ant-test"), request)
878  let assert Ok("sk-ant-test") = list.key_find(exchange.headers, "x-api-key")
879  let assert Error(Nil) = list.key_find(exchange.headers, "authorization")
880  let assert Error(Nil) = list.key_find(exchange.headers, "x-app")
881  let assert Ok(betas) = list.key_find(exchange.headers, "anthropic-beta")
882  assert !string.contains(betas, "claude-code")
883  assert !string.contains(betas, "oauth")
884  let text = sent(exchange.body)
885  assert !string.contains(text, "cch=")
886  let assert Ok(value) = json.parse(text, decode.dynamic)
887  let absent = fn(field) {
888    decode.run(
889      value,
890      decode.optional_field(field, True, decode.success(False), decode.success),
891    )
892  }
893  let assert Ok(True) = absent("metadata")
894  let assert Ok(["You are Claude Code, Anthropic's official CLI for Claude."]) =
895    decode.run(
896      value,
897      decode.at(["system"], decode.list(decode.at(["text"], decode.string))),
898    )
899}
900
901@external(erlang, "albedo_claude_billing", "hash")
902fn billing_hash(body: String) -> String
903
904@external(erlang, "albedo_claude_billing", "hash_streamed")
905fn billing_hash_streamed(chunks: List(String)) -> String
906
907@external(erlang, "albedo_openai_transport", "materialize")
908fn materialize(body: StringTree) -> Result(String, String)
909
910fn sent(body: StringTree) -> String {
911  let assert Ok(text) = materialize(body)
912  text
913}
914
915@external(erlang, "albedo_claude_test_support", "stored_image")
916fn stored_image(
917  mime: String,
918  hash: String,
919  size: Int,
920  read: fn() -> Result(String, Nil),
921  width: Int,
922  height: Int,
923  bytes: Int,
924) -> types.Image
925
926fn feed_all(
927  reducer: reducer.Reducer,
928  chunks: List(String),
929) -> Result(types.Turn, types.Error) {
930  case chunks {
931    [] -> Error(types.UnexpectedEnd)
932    [chunk, ..rest] -> {
933      use #(next, _, turn) <- result.try(reducer.feed(chunk))
934      case turn {
935        Some(turn) -> Ok(turn)
936        None -> feed_all(next, rest)
937      }
938    }
939  }
940}
941
942fn list_first(items: List(types.ReplayItem)) -> types.ReplayItem {
943  let assert [first, ..] = items
944  first
945}
946
947@external(erlang, "albedo_skills_test_support", "fixture")
948fn fixture() -> #(String, String, String)
949
950@external(erlang, "albedo_skills_test_support", "write")
951fn write(base: String, path: String, contents: String) -> String
952
953@external(erlang, "albedo_skills_test_support", "cleanup")
954fn cleanup(root: String) -> Nil
955
956@external(erlang, "albedo_claude_auth", "accounts")
957fn claude_accounts(home: String) -> List(Dynamic)
958
959// A legacy anthropic account stored without a refresh token crashed quota
960// enumeration with `bad key: <<"refresh">>` in refresh_current/3, taking out
961// every Claude account's readings; E2E only ever sees that crash as log noise
962// from a background spawn, so the direct call is the only clean reach. Such an
963// account is never refreshed and stays as stored, like a refresh that failed,
964// beside its healthy sibling.
965pub fn claude_accounts_without_a_refresh_token_stay_enumerable_test() -> Nil {
966  let #(root, _, home) = fixture()
967  let _ =
968    write(
969      home,
970      "creds.json",
971      "{\"accounts\":{\"anthropic\":[{\"type\":\"oauth\",\"access\":\"legacy-access\"},"
972        <> "{\"type\":\"oauth\",\"access\":\"fresh-access\",\"refresh\":\"r\","
973        <> "\"expires\":4102444800000}]}}",
974    )
975  let access = decode.at(["access"], decode.string)
976  let assert [Ok("legacy-access"), Ok("fresh-access")] =
977    list.map(claude_accounts(home), decode.run(_, access))
978  cleanup(root)
979}
980
981// Claude's fixed authenticated endpoint cannot receive the E2E fixture payloads.
982pub fn malformed_stream_diagnostics_preserve_types_without_payloads_test() -> Nil {
983  let reducer = stream.reducer("claude-opus-5-5", [])
984  let assert Error(types.InvalidEvent(message)) =
985    reducer.feed(
986      "{\"type\":\"content_block_delta\",\"index\":\"secret-provider-value\",\"delta\":{\"type\":\"text_delta\"}}",
987    )
988  assert string.contains(message, "$.index")
989  assert string.contains(message, "expected Int, found String")
990  assert !string.contains(message, "secret-provider-value")
991  let assert Error(types.InvalidEvent(message)) =
992    reducer.feed("{\"type\":\"secret-provider-value")
993  assert string.contains(message, "unexpected end of JSON")
994  assert !string.contains(message, "secret-provider-value")
995}