okami.mom/albedo

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

dawnantigravity: limit marks per model-family metere9df547

main
21.4 KiB590 linesraw
1// Signed replay and provider schema policy are tested offline because Antigravity
2// uses an authenticated fixed endpoint that the loopback E2E provider cannot reach.
3import albedo/harness/extensions/antigravity/catalog
4import albedo/harness/extensions/antigravity/stream
5import albedo/harness/extensions/antigravity/wire
6import albedo/openai_api
7import albedo/openai_api/stream as provider_stream
8import albedo/openai_api/types
9import gleam/dynamic
10import gleam/dynamic/decode
11import gleam/int
12import gleam/json
13import gleam/list
14import gleam/option.{type Option, None, Some}
15import gleam/string
16import gleam/string_tree
17
18const gemini = "gemini-3.1-pro-low"
19
20const claude = "claude-sonnet-4-6"
21
22/// Test fixture models for wire encoding and streaming tests.
23fn model(id: String) -> catalog.Model {
24  case id {
25    "gemini-3.1-pro-low" ->
26      catalog.Model(
27        id,
28        "Gemini 3.1 Pro (Low)",
29        1_048_576,
30        65_535,
31        True,
32        catalog.Budget(1001, 8192, 10_001),
33        Some("MODEL_PLACEHOLDER_M36"),
34        True,
35      )
36    "claude-sonnet-4-6" ->
37      catalog.Model(
38        id,
39        "Claude Sonnet 4.6",
40        250_000,
41        64_000,
42        True,
43        catalog.Budget(4096, 8192, 16_384),
44        None,
45        True,
46      )
47    _ -> catalog.model("/nonexistent/albedo-home", id, None)
48  }
49}
50
51fn context(id: String) -> wire.Context {
52  wire.Context(
53    "token-1",
54    "project-1",
55    "session-1",
56    model(id),
57    "antigravity/hub/test",
58  )
59}
60
61fn body(model: String, request: types.Request) -> dynamic.Dynamic {
62  let assert Ok(openai_api.Exchange(body: body, ..)) =
63    wire.encode(context(model), request)
64  let assert Ok(value) = json.parse(string_tree.to_string(body), decode.dynamic)
65  value
66}
67
68fn at(
69  value: dynamic.Dynamic,
70  path: List(String),
71  decoder: decode.Decoder(a),
72) -> a {
73  let assert Ok(found) = decode.run(value, decode.at(path, decoder))
74  found
75}
76
77fn schema_json(source: String) -> json.Json {
78  let assert Ok(value) = json.parse(source, decode.dynamic)
79  types.encode_value(value)
80}
81
82fn schema_request(id: String, schema: json.Json) -> types.Request {
83  types.Request(
84    ..openai_api.request(id, [types.User("hi")]),
85    tools: [types.Tool("inputs", "Choose inputs", schema, False)],
86    options: types.Options(
87      ..types.defaults,
88      format: Some(types.JsonSchema("answer", schema, True)),
89    ),
90  )
91}
92
93fn tool_schema(value: dynamic.Dynamic) -> dynamic.Dynamic {
94  let assert [tool] =
95    at(value, ["request", "tools"], decode.list(decode.dynamic))
96  let assert [declaration] =
97    at(tool, ["functionDeclarations"], decode.list(decode.dynamic))
98  at(declaration, ["parameters"], decode.dynamic)
99}
100
101pub fn schema_resolves_references_and_merges_object_requirements_test() -> Result(
102  dynamic.Dynamic,
103  List(decode.DecodeError),
104) {
105  let schema =
106    schema_json(
107      "{
108    \"definitions\": {\"child\": {\"type\": \"boolean\"}},
109    \"$defs\": {\"child\": {\"type\": \"object\", \"properties\": {
110      \"value\": {\"type\": \"integer\"}
111    }, \"required\": [\"value\", \"missing\", 12]}},
112    \"properties\": {\"shared\": {\"type\": \"string\"}},
113    \"required\": [\"shared\", \"absent\", false],
114    \"allOf\": [
115      {\"title\": \"earlier\", \"properties\": {
116        \"shared\": {\"type\": \"number\"},
117        \"branch\": {\"type\": \"boolean\"},
118        \"children\": {\"type\": \"array\", \"items\": {\"$ref\": \"#/arbitrary/path/child\"}}
119      }, \"required\": [\"children\", \"branch\"]},
120      {\"title\": \"later\", \"properties\": {\"branch\": {\"type\": \"string\"}},
121       \"required\": [\"shared\", \"branch\", null]}
122    ]
123  }",
124    )
125  let actual = tool_schema(body(gemini, schema_request(gemini, schema)))
126  assert at(actual, ["type"], decode.string) == "object"
127  assert at(actual, ["title"], decode.string) == "later"
128  assert at(actual, ["properties", "shared", "type"], decode.string) == "string"
129  assert at(actual, ["properties", "branch", "type"], decode.string)
130    == "boolean"
131  assert at(actual, ["required"], decode.list(decode.string))
132    == ["branch", "children", "shared"]
133  let child = at(actual, ["properties", "children", "items"], decode.dynamic)
134  assert at(child, ["type"], decode.string) == "object"
135  assert at(child, ["properties", "value", "type"], decode.string) == "integer"
136  assert at(child, ["required"], decode.list(decode.string)) == ["value"]
137  let assert Error(_) = decode.run(actual, decode.at(["allOf"], decode.dynamic))
138}
139
140pub fn schema_reduces_unions_and_keeps_constraint_guidance_test() -> Result(
141  dynamic.Dynamic,
142  List(decode.DecodeError),
143) {
144  let schema =
145    schema_json(
146      "{\"type\": \"object\", \"properties\": {
147    \"nullable\": {\"anyOf\": [{\"type\": \"null\"}, {\"type\": \"integer\"}]},
148    \"typed\": {\"type\": [null, \"null\", \"boolean\", \"string\"]},
149    \"choice\": {\"oneOf\": [{\"const\": 7}, {\"enum\": [true, \"seven\", null, 7]}]},
150    \"constant\": {\"const\": false},
151    \"mixed\": {\"description\": \"Pick\", \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"number\"}]},
152    \"limited\": {\"type\": \"string\", \"description\": \"Name\", \"pattern\": \"^[a-z]+$\", \"minLength\": 2}
153  }}",
154    )
155  let actual = tool_schema(body(gemini, schema_request(gemini, schema)))
156  let properties = at(actual, ["properties"], decode.dynamic)
157  assert at(properties, ["nullable", "type"], decode.string) == "integer"
158  assert at(properties, ["typed", "type"], decode.string) == "boolean"
159  assert at(properties, ["choice", "type"], decode.string) == "string"
160  assert at(properties, ["choice", "enum"], decode.list(decode.string))
161    == ["7", "seven", "true"]
162  assert at(properties, ["constant", "enum"], decode.list(decode.string))
163    == ["false"]
164  assert at(properties, ["mixed", "type"], decode.string) == "string"
165  assert at(properties, ["mixed", "description"], decode.string)
166    == "Pick (one of: string, number)"
167  let limited = at(properties, ["limited"], decode.dynamic)
168  let guidance = at(limited, ["description"], decode.string)
169  assert string.contains(guidance, "Name")
170  assert string.contains(guidance, "pattern: \"^[a-z]+$\"")
171  assert string.contains(guidance, "minLength: 2")
172  let assert Error(_) =
173    decode.run(limited, decode.at(["pattern"], decode.dynamic))
174}
175
176fn item_depth(schema: dynamic.Dynamic) -> Int {
177  case decode.run(schema, decode.at(["items"], decode.dynamic)) {
178    Ok(items) -> 1 + item_depth(items)
179    Error(_) -> 0
180  }
181}
182
183pub fn schema_bounds_recursion_and_handles_malformed_keywords_test() -> Nil {
184  let malformed =
185    schema_json(
186      "{
187    \"type\": \"object\", \"description\": false, \"title\": [], \"required\": true,
188    \"definitions\": [], \"$defs\": {\"loop\": {\"$ref\": \"#/$defs/loop\"}},
189    \"allOf\": [null, false, {\"properties\": [], \"required\": false}],
190    \"properties\": {
191      \"cycle\": {\"$ref\": \"#/$defs/loop\"},
192      \"object\": {\"type\": \"object\", \"properties\": [], \"required\": true},
193      \"array\": {\"type\": \"array\", \"items\": [false, {\"type\": \"string\"}]},
194      \"union\": {\"oneOf\": [3, {\"title\": false}, {\"title\": []}]},
195      \"enum\": {\"type\": \"string\", \"enum\": false},
196      \"combiner\": {\"anyOf\": false, \"allOf\": {}, \"oneOf\": 2}
197    }
198  }",
199    )
200  let actual = tool_schema(body(gemini, schema_request(gemini, malformed)))
201  let assert Ok(empty) = json.parse("{}", decode.dynamic)
202  assert at(actual, ["properties", "cycle"], decode.dynamic) == empty
203  assert at(actual, ["properties", "object", "properties"], decode.dynamic)
204    == empty
205  let assert Error(_) =
206    decode.run(actual, decode.at(["required"], decode.dynamic))
207  assert at(actual, ["title"], decode.string) == ""
208  assert at(actual, ["description"], decode.string) == ""
209  let assert Error(_) =
210    decode.run(
211      actual,
212      decode.at(["properties", "enum", "enum"], decode.dynamic),
213    )
214  assert at(actual, ["properties", "combiner"], decode.dynamic) == empty
215  assert at(actual, ["properties", "union", "description"], decode.string)
216    == "one of: schema, schema"
217  assert at(actual, ["properties", "array", "items", "type"], decode.string)
218    == "string"
219  let deep =
220    list.fold(list.repeat(Nil, 40), json.object([]), fn(items, _) {
221      json.object([#("type", json.string("array")), #("items", items)])
222    })
223  let schema =
224    json.object([
225      #("type", json.string("object")),
226      #("properties", json.object([#("deep", deep)])),
227    ])
228  let actual = tool_schema(body(gemini, schema_request(gemini, schema)))
229  let depth = item_depth(at(actual, ["properties", "deep"], decode.dynamic))
230  assert depth > 0
231  assert depth <= 32
232  let definitions =
233    int.range(0, 41, [], fn(definitions, index) {
234      let target = case index {
235        40 ->
236          json.object([
237            #("type", json.string("object")),
238            #(
239              "properties",
240              json.object([
241                #(
242                  "unreachable",
243                  json.object([#("type", json.string("string"))]),
244                ),
245              ]),
246            ),
247          ])
248        _ ->
249          json.object([
250            #("$ref", json.string("#/$defs/" <> int.to_string(index + 1))),
251          ])
252      }
253      [#(int.to_string(index), target), ..definitions]
254    })
255  let schema =
256    json.object([
257      #("$ref", json.string("#/$defs/0")),
258      #("$defs", json.object(definitions)),
259    ])
260  let actual = tool_schema(body(gemini, schema_request(gemini, schema)))
261  assert at(actual, ["type"], decode.string) == "object"
262  assert at(actual, ["properties"], decode.dynamic) == empty
263}
264
265pub fn both_model_families_normalize_tools_and_structured_responses_test() -> Nil {
266  let schemas = [
267    #(
268      schema_json(
269        "{\"properties\": {\"answer\": {\"const\": 42}}, \"required\": [\"answer\", \"missing\"]}",
270      ),
271      True,
272    ),
273    #(json.bool(True), False),
274    #(
275      schema_json("{\"type\": \"array\", \"items\": {\"type\": \"string\"}}"),
276      False,
277    ),
278  ]
279  list.each([gemini, claude], fn(id) {
280    list.each(schemas, fn(entry) {
281      let #(schema, has_answer) = entry
282      let value = body(id, schema_request(id, schema))
283      let parameters = tool_schema(value)
284      let response =
285        at(
286          value,
287          ["request", "generationConfig", "responseSchema"],
288          decode.dynamic,
289        )
290      assert parameters == response
291      assert at(response, ["type"], decode.string) == "object"
292      assert at(
293          value,
294          ["request", "generationConfig", "responseMimeType"],
295          decode.string,
296        )
297        == "application/json"
298      case has_answer {
299        True -> {
300          assert at(
301              response,
302              ["properties", "answer", "enum"],
303              decode.list(decode.string),
304            )
305            == ["42"]
306          assert at(response, ["required"], decode.list(decode.string))
307            == ["answer"]
308        }
309        False -> {
310          let assert Ok(empty) = json.parse("{}", decode.dynamic)
311          assert at(response, ["properties"], decode.dynamic) == empty
312        }
313      }
314    })
315  })
316}
317
318fn chunk(json: String) -> String {
319  "{\"response\":" <> json <> ",\"traceId\":\"t\"}"
320}
321
322/// Feeds each payload, then ends the body.
323fn reduce(
324  id: String,
325  payloads: List(String),
326) -> #(Result(types.Turn, types.Error), List(types.Event)) {
327  let #(reducer, events) =
328    list.fold(payloads, #(stream.reducer(model(id)), []), fn(acc, data) {
329      let #(reducer, events) = acc
330      let assert Ok(#(next, new, None)) = reducer.feed(data)
331      #(next, list.append(events, new))
332    })
333  #(reducer.finish(), events)
334}
335
336const signed_turn = [
337  "{\"responseId\":\"r1\",\"candidates\":[{\"content\":{\"role\":\"model\",\"parts\":[{\"text\":\"weigh\",\"thought\":true},{\"text\":\"ing\",\"thought\":true,\"thoughtSignature\":\"c2ln\"}]}}]}",
338  "{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"looking\"},{\"functionCall\":{\"name\":\"bash\",\"args\":{\"command\":\"ls\"}},\"thoughtSignature\":\"Y2FsbA==\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":10,\"candidatesTokenCount\":3,\"thoughtsTokenCount\":2,\"cachedContentTokenCount\":4}}",
339]
340
341fn signed_turn_payloads() -> List(String) {
342  list.map(signed_turn, chunk)
343}
344
345pub fn stream_reduces_thoughts_text_and_calls_test() -> Nil {
346  let #(outcome, events) = reduce(gemini, signed_turn_payloads())
347  let assert Ok(turn) = outcome
348  let assert [
349    types.Started("r1"),
350    types.ThinkingDelta("weigh"),
351    types.ThinkingDelta("ing"),
352    types.TextDelta(0, 0, "looking"),
353    types.ArgumentsDelta(0, "bash", arguments),
354  ] = events
355  assert arguments == "{\"command\":\"ls\"}"
356  assert turn.finish == types.ToolCalls
357  // Output folds the 2 thought tokens in; reasoning carries the thought count.
358  assert turn.usage
359    == Some(types.Usage(10, 5, Some(4), None, None, None, Some(2)))
360  let assert [types.ToolCall(id, "bash", "{\"command\":\"ls\"}")] =
361    turn.tool_calls
362  assert string.starts_with(id, "call_")
363  assert turn.call_indices == [#(id, 0)]
364  let assert [item] = turn.output
365  assert types.inspect_item(item, decode.at(["content"], decode.string))
366    == Ok("looking")
367  assert types.inspect_item(
368      item,
369      decode.at(["reasoning_content"], decode.string),
370    )
371    == Ok("weighing")
372}
373
374pub fn stream_maps_multiple_calls_to_their_emitted_output_indices_test() -> Nil {
375  let payload =
376    chunk(
377      "{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"bash\",\"args\":{\"command\":\"ls\"}}},{\"functionCall\":{\"name\":\"bash\",\"args\":{\"command\":\"pwd\"}}}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":10,\"candidatesTokenCount\":3}}",
378    )
379  let #(outcome, events) = reduce(gemini, [payload])
380  let assert Ok(turn) = outcome
381  let assert [first, second] = turn.tool_calls
382  assert turn.call_indices == [#(first.id, 0), #(second.id, 1)]
383  assert events
384    == [
385      types.ArgumentsDelta(0, "bash", "{\"command\":\"ls\"}"),
386      types.ArgumentsDelta(1, "bash", "{\"command\":\"pwd\"}"),
387    ]
388}
389
390pub fn same_model_replay_keeps_signed_parts_test() -> Nil {
391  let #(outcome, _) = reduce(gemini, signed_turn_payloads())
392  let assert Ok(turn) = outcome
393  let assert [item] = turn.output
394  let assert [call] = turn.tool_calls
395  let request =
396    openai_api.request(gemini, [
397      types.User("hi"),
398      types.Replay(item),
399      types.ToolOutput(call.id, "a b", []),
400    ])
401  let contents =
402    at(
403      body(gemini, request),
404      ["request", "contents"],
405      decode.list(decode.dynamic),
406    )
407  let assert [_, model, result] = contents
408  let parts = at(model, ["parts"], decode.list(decode.dynamic))
409  let assert [thought, text, function] = parts
410  assert at(thought, ["thought"], decode.bool)
411  assert at(thought, ["text"], decode.string) == "weighing"
412  assert at(thought, ["thoughtSignature"], decode.string) == "c2ln"
413  assert at(text, ["text"], decode.string) == "looking"
414  assert at(function, ["thoughtSignature"], decode.string) == "Y2FsbA=="
415  assert at(function, ["functionCall", "args", "command"], decode.string)
416    == "ls"
417  // Gemini correlates by name; ids are only for Claude routes.
418  let assert Error(_) =
419    decode.run(function, decode.at(["functionCall", "id"], decode.string))
420  assert at(result, ["role"], decode.string) == "user"
421  assert at(
422      result,
423      ["parts"],
424      decode.list(decode.at(["functionResponse", "name"], decode.string)),
425    )
426    == ["bash"]
427}
428
429pub fn another_models_output_replays_portably_test() -> Nil {
430  let #(outcome, _) = reduce(gemini, signed_turn_payloads())
431  let assert Ok(turn) = outcome
432  let assert [item] = turn.output
433  let assert [call] = turn.tool_calls
434  let request =
435    openai_api.request(claude, [
436      types.User("hi"),
437      types.Replay(item),
438      types.ToolOutput(call.id, "", []),
439    ])
440  let request_body = body(claude, request)
441  let assert [_, model, result] =
442    at(request_body, ["request", "contents"], decode.list(decode.dynamic))
443  let assert [text, function] =
444    at(model, ["parts"], decode.list(decode.dynamic))
445  assert at(text, ["text"], decode.string) == "looking"
446  assert at(function, ["functionCall", "id"], decode.string) == call.id
447  let assert Error(_) =
448    decode.run(function, decode.at(["thoughtSignature"], decode.string))
449  assert at(
450      result,
451      ["parts"],
452      decode.list(decode.at(["functionResponse", "id"], decode.string)),
453    )
454    == [call.id]
455  assert at(
456      request_body,
457      ["request", "toolConfig", "functionCallingConfig", "mode"],
458      decode.string,
459    )
460    == "VALIDATED"
461}
462
463pub fn foreign_calls_carry_the_skip_signature_on_gemini_test() -> Nil {
464  let assert Ok(item) =
465    json.parse(
466      "{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\",\"function\":{\"name\":\"bash\",\"arguments\":\"{}\"}}]}",
467      types.replay_decoder(types.ChatCompletions),
468    )
469  let request =
470    openai_api.request(gemini, [
471      types.User("hi"),
472      types.Replay(item),
473      types.ToolOutput("c1", "ok", []),
474    ])
475  let assert [_, model, _] =
476    at(
477      body(gemini, request),
478      ["request", "contents"],
479      decode.list(decode.dynamic),
480    )
481  assert at(
482      model,
483      ["parts"],
484      decode.list(decode.at(["thoughtSignature"], decode.string)),
485    )
486    == ["skip_thought_signature_validator"]
487}
488
489pub fn frame_archive_images_carry_no_empty_text_part_test() -> Nil {
490  // Snapcompact's frame archive attaches images with no text after the
491  // archive prompt; Cloud Code Assist's Claude translation rejected the
492  // empty text part with "messages.0.content.2.text.text: Field required".
493  let assert Ok(first) = types.image("image/png", "aGk=", 2, 3, 2)
494  let assert Ok(second) = types.image("image/png", "aGk=", 2, 3, 2)
495  let request =
496    openai_api.request(claude, [
497      types.UserImage("the archive prompt", [first]),
498      types.UserImage("", [second]),
499    ])
500  let assert [user] =
501    at(
502      body(claude, request),
503      ["request", "contents"],
504      decode.list(decode.dynamic),
505    )
506  assert at(user, ["role"], decode.string) == "user"
507  let assert [text, first_part, second_part] =
508    at(user, ["parts"], decode.list(decode.dynamic))
509  assert at(text, ["text"], decode.string) == "the archive prompt"
510  assert at(first_part, ["inlineData", "mimeType"], decode.string)
511    == "image/png"
512  assert at(second_part, ["inlineData", "mimeType"], decode.string)
513    == "image/png"
514  let assert Error(_) =
515    decode.run(second_part, decode.at(["text"], decode.string))
516  Nil
517}
518
519pub fn empty_body_is_a_retryable_failure_test() -> Nil {
520  let #(outcome, _) = reduce(gemini, [])
521  assert outcome == Error(types.UnexpectedEnd)
522}
523
524pub fn stream_errors_surface_the_provider_message_test() -> Result(
525  #(provider_stream.Reducer, List(types.Event), option.Option(types.Turn)),
526  types.Error,
527) {
528  let reducer = stream.reducer(model(gemini))
529  let assert Error(types.ProviderError("quota")) =
530    reducer.feed("{\"error\":{\"code\":429,\"message\":\"quota\"}}")
531}
532
533pub type LimitResult {
534  LimitResult(until: Int, lasting: Bool, meter: Option(String))
535}
536
537@external(erlang, "albedo_antigravity", "limit")
538fn native_limit(body: String) -> Result(LimitResult, Nil)
539
540@external(erlang, "albedo_antigravity", "limit")
541fn native_limit_fallback(
542  body: String,
543  fallback: Option(String),
544) -> Result(LimitResult, Nil)
545
546pub fn limit_extracts_claude_meter_from_metadata_model_test() -> Nil {
547  let body =
548    "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"model\":\"claude-sonnet-5-5-high\",\"quotaResetDelay\":\"60s\"}}]}}"
549  let assert Ok(result) = native_limit(body)
550  assert result.meter == Some("claude")
551  assert result.lasting == True
552}
553
554pub fn limit_extracts_gpt_oss_meter_from_metadata_model_test() -> Nil {
555  let body =
556    "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"model\":\"gpt-oss-120b-medium\",\"quotaResetDelay\":\"60s\"}}]}}"
557  let assert Ok(result) = native_limit(body)
558  assert result.meter == Some("gpt-oss")
559  assert result.lasting == True
560}
561
562pub fn limit_extracts_gemini_meter_from_metadata_model_test() -> Nil {
563  let body =
564    "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"model\":\"gemini-3.8-flash-medium\",\"quotaResetDelay\":\"60s\"}}]}}"
565  let assert Ok(result) = native_limit(body)
566  assert result.meter == Some("gemini")
567  assert result.lasting == True
568}
569
570pub fn limit_falls_back_to_request_family_when_metadata_model_missing_test() -> Nil {
571  let body =
572    "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"quotaResetDelay\":\"60s\"}}]}}"
573  let assert Ok(claude_result) = native_limit_fallback(body, Some("claude"))
574  assert claude_result.meter == Some("claude")
575
576  let assert Ok(gemini_result) = native_limit_fallback(body, Some("gemini"))
577  assert gemini_result.meter == Some("gemini")
578
579  let assert Ok(gpt_result) = native_limit_fallback(body, Some("gpt-oss"))
580  assert gpt_result.meter == Some("gpt-oss")
581
582  let assert Ok(no_fallback) = native_limit(body)
583  assert no_fallback.meter == None
584}
585
586pub fn model_capacity_exhausted_marks_nobody_test() -> Nil {
587  let body =
588    "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"MODEL_CAPACITY_EXHAUSTED\",\"metadata\":{\"model\":\"claude-sonnet-5-5-high\"}}]}}"
589  assert native_limit(body) == Error(Nil)
590}