okami.mom/albedo

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

dawnantigravity: limit marks per model-family metere9df547

main
15.9 KiB441 linesraw
1//// Account quota, capacity routing, and exact pool ordering require provider
2//// limit states and captured clocks the E2E fake provider cannot reproduce.
3
4import albedo/clock
5import albedo/harness/accounts
6import albedo/harness/extension as harness_extension
7import albedo/harness/extensions/alibaba/extension as alibaba
8import albedo/harness/extensions/antigravity/extension as antigravity
9import albedo/harness/oauth
10import albedo/harness/rotation
11import albedo/openai_api
12import albedo/openai_api/types
13import gleam/dict.{type Dict}
14import gleam/dynamic.{type Dynamic}
15import gleam/erlang/process
16import gleam/int
17import gleam/list
18import gleam/option.{None, Some}
19import gleam/string
20import gleeunit/should
21
22const google_accounts =
23  "{\"accounts\":{\"google-antigravity\":[{\"type\":\"oauth\",\"access\":\"a1\",\"refresh\":\"r1\",\"expires\":9999999999999,\"projectId\":\"p1\",\"email\":\"one@example.com\"},{\"type\":\"oauth\",\"access\":\"a2\",\"refresh\":\"r2\",\"expires\":9999999999999,\"projectId\":\"p2\",\"email\":\"two@example.com\"}]}}"
24
25const claude_quota =
26  "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"message\":\"You have exhausted your capacity on this model.\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"model\":\"claude-sonnet-5-5-high\",\"quotaResetDelay\":\"2h3m4.5s\"}},{\"@type\":\"type.googleapis.com/google.rpc.RetryInfo\",\"retryDelay\":\"7384.5s\"}]}}"
27
28const gemini_quota =
29  "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"message\":\"You have exhausted your capacity on this model.\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"model\":\"gemini-3.8-flash-medium\",\"quotaResetDelay\":\"2h3m4.5s\"}},{\"@type\":\"type.googleapis.com/google.rpc.RetryInfo\",\"retryDelay\":\"7384.5s\"}]}}"
30
31const quota =
32  "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"message\":\"You have exhausted your capacity on this model.\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"QUOTA_EXHAUSTED\",\"metadata\":{\"quotaResetDelay\":\"2h3m4.5s\"}},{\"@type\":\"type.googleapis.com/google.rpc.RetryInfo\",\"retryDelay\":\"7384.5s\"}]}}"
33
34const capacity =
35  "{\"error\":{\"code\":429,\"status\":\"RESOURCE_EXHAUSTED\",\"message\":\"No capacity available for model\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"MODEL_CAPACITY_EXHAUSTED\"}]}}"
36
37fn request() -> types.Request {
38  openai_api.request("gemini-3-flash", [types.User("hi")])
39}
40
41/// Answers 429 with `body` for the listed accounts and fails plainly for any
42/// other, recording each account tried.
43fn limited_stream(
44  limited: List(String),
45  body: String,
46  tried: process.Subject(String),
47  key: fn(account) -> String,
48) -> fn(account, a, b) -> Result(c, types.Error) {
49  fn(account, _request, _on_event) {
50    process.send(tried, key(account))
51    case list.contains(limited, key(account)) {
52      True -> Error(types.HttpError(429, body))
53      False -> Error(types.HttpError(500, "sibling answered"))
54    }
55  }
56}
57
58fn drain(tried: process.Subject(String), found: List(String)) -> List(String) {
59  case process.receive(tried, 0) {
60    Ok(token) -> drain(tried, [token, ..found])
61    Error(_) -> list.reverse(found)
62  }
63}
64
65fn no_wait(_: Int) -> Nil {
66  panic as "a sibling had room; nothing should wait"
67}
68
69fn token(access: antigravity.Access) -> String {
70  access.token
71}
72
73pub fn antigravity_sessions_spread_and_stick_test() -> Nil {
74  let #(root, _, home) = fixture()
75  let _ = write(home, "creds.json", google_accounts)
76  let pool = fn(session) {
77    antigravity.pool(home, session, fn(_, _, _) { Error(types.Timeout) })
78  }
79  let assert Ok(first) = { pool("session-1") }.current()
80  let assert Ok(again) = { pool("session-1") }.current()
81  again |> should.equal(first)
82  let assert Ok(other) = { pool("session-2") }.current()
83  { other.token == first.token } |> should.be_false
84  cleanup(root)
85}
86
87pub fn antigravity_quota_moves_the_request_to_a_sibling_test() -> Nil {
88  let #(root, _, home) = fixture()
89  let _ = write(home, "creds.json", google_accounts)
90  let tried = process.new_subject()
91  let pool =
92    antigravity.pool(
93      home,
94      "quota-session",
95      limited_stream(["a1", "a2"], quota, tried, token),
96    )
97  let assert Ok(first) = pool.current()
98  rotation.stream(
99    pool,
100    first,
101    request(),
102    fn(_) { types.Continue },
103    rotation.Budget(8, 0, fn(_) {
104      panic as "a spent quota lasts hours; it must not be waited on"
105    }),
106    fn(_) { Nil },
107  )
108  |> should.equal(Error(types.HttpError(429, quota)))
109  list.length(drain(tried, [])) |> should.equal(2)
110
111  // Both accounts show their limit in /login, and the failure says so.
112  let assert [one, two] = oauth.accounts(home, antigravity.login(google()))
113  string.contains(one.detail, "limited until") |> should.be_true
114  string.contains(two.detail, "limited until") |> should.be_true
115  let assert Ok(last) = pool.current()
116  let assert Some(message) =
117    antigravity.explain(home, last, types.HttpError(429, quota))
118  string.contains(message, "quota exhausted") |> should.be_true
119  string.contains(message, "no other Google account") |> should.be_true
120  cleanup(root)
121}
122
123/// Claude behind Cloud Code Assist is held to Anthropic's 2000px rule, so an
124/// oversized image is fitted before the request rather than refused after it.
125pub fn antigravity_claude_quota_leaves_gemini_served_test() -> Nil {
126  let #(root, _, home) = fixture()
127  let _ = write(home, "creds.json", google_accounts)
128  let tried = process.new_subject()
129  let claude_pool =
130    antigravity.pool_for_model(
131      home,
132      "claude-session",
133      "claude-sonnet-4-6",
134      limited_stream(["a1"], claude_quota, tried, token),
135    )
136  let assert Ok(first) = claude_pool.current()
137  first.token |> should.equal("a1")
138
139  // Account a1 hits a Claude 429 and hops to sibling a2.
140  rotation.stream(
141    claude_pool,
142    first,
143    openai_api.request("claude-sonnet-4-6", [types.User("hi")]),
144    fn(_) { types.Continue },
145    rotation.Budget(8, 0, no_wait),
146    fn(_) { Nil },
147  )
148  |> should.equal(Error(types.HttpError(500, "sibling answered")))
149  let attempts = drain(tried, [])
150  attempts |> should.equal(["a1", "a2"])
151
152  // Claude requests now treat a1 as limited and stick to a2.
153  let assert Ok(claude_next) = claude_pool.current()
154  claude_next.token |> should.equal("a2")
155
156  // But Gemini requests on the same session/account are NOT walled: a1 still serves Gemini!
157  let gemini_pool =
158    antigravity.pool_for_model(home, "gemini-1", "gemini-3-flash", fn(_, _, _) {
159      Error(types.Timeout)
160    })
161  let assert Ok(gemini_served) = gemini_pool.current()
162  gemini_served.token |> should.equal("a1")
163
164  // /login shows a1 is limited specifically on Claude.
165  let assert [one, two] = oauth.accounts(home, antigravity.login(google()))
166  string.contains(one.detail, "limited on Claude until") |> should.be_true
167  string.contains(two.detail, "limited on Claude until") |> should.be_false
168
169  // When all accounts exhaust their Claude quota, explain names the Claude meter.
170  let both_tried = process.new_subject()
171  let both_claude_pool =
172    antigravity.pool_for_model(
173      home,
174      "both-claude-session",
175      "claude-sonnet-4-6",
176      limited_stream(["a1", "a2"], claude_quota, both_tried, token),
177    )
178  let assert Ok(c1) = both_claude_pool.current()
179  rotation.stream(
180    both_claude_pool,
181    c1,
182    openai_api.request("claude-sonnet-4-6", [types.User("hi")]),
183    fn(_) { types.Continue },
184    rotation.Budget(8, 0, fn(_) {
185      panic as "a spent quota lasts hours; it must not be waited on"
186    }),
187    fn(_) { Nil },
188  )
189  |> should.equal(Error(types.HttpError(429, claude_quota)))
190  let assert Ok(last) = both_claude_pool.current()
191  let assert Some(claude_msg) =
192    antigravity.explain(home, last, types.HttpError(429, claude_quota))
193  string.contains(claude_msg, "Antigravity Claude quota exhausted")
194  |> should.be_true
195  string.contains(claude_msg, "its claude meter") |> should.be_true
196  string.contains(
197    claude_msg,
198    "no other Google account has room on the Claude meter",
199  )
200  |> should.be_true
201
202  // Both a1 and a2 are now limited on Claude. When a2 also exhausts Gemini, and
203  // then a1 exhausts Gemini, no accounts have room on Gemini, and refusal message names
204  // the Gemini meter while mentioning sibling a2's Claude limit.
205  let assert Some(_) =
206    antigravity.explain(home, claude_next, types.HttpError(429, gemini_quota))
207  let assert Some(gemini_msg) =
208    antigravity.explain(home, first, types.HttpError(429, gemini_quota))
209  string.contains(
210    gemini_msg,
211    "Antigravity Gemini quota exhausted for one@example.com",
212  )
213  |> should.be_true
214  string.contains(gemini_msg, "its gemini meter") |> should.be_true
215  string.contains(
216    gemini_msg,
217    "no other Google account has room on the Gemini meter.",
218  )
219  |> should.be_true
220  string.contains(gemini_msg, "two@example.com is limited on Claude until")
221  |> should.be_true
222
223  cleanup(root)
224}
225
226pub fn antigravity_claude_declares_anthropic_image_limits_test() -> Nil {
227  let #(root, _, home) = fixture()
228  let _ = write(home, "creds.json", google_accounts)
229  let assert [harness_extension.ModelProviderPlugin(provider)] =
230    list.filter(antigravity.extension().plugins, fn(plugin) {
231      case plugin {
232        harness_extension.ModelProviderPlugin(_) -> True
233        _ -> False
234      }
235    })
236  let images = fn(model) {
237    let assert Some(Ok(upstream)) =
238      provider.resolve(harness_extension.ModelContext(
239        home,
240        "image-session",
241        "",
242        "antigravity",
243        model,
244        types.ChatCompletions,
245        None,
246      ))
247    upstream.images
248  }
249  images("claude-sonnet-4-6") |> should.equal(types.anthropic_images)
250  images("gemini-3-flash") |> should.equal(types.any_images)
251  cleanup(root)
252}
253
254pub fn antigravity_hands_off_when_one_account_is_spent_test() -> Nil {
255  let #(root, _, home) = fixture()
256  let _ = write(home, "creds.json", google_accounts)
257  let tried = process.new_subject()
258  let pool =
259    antigravity.pool(
260      home,
261      "handoff-session",
262      limited_stream(["a1", "a2"], quota, tried, token),
263    )
264  let assert Ok(first) = pool.current()
265  let pool =
266    rotation.Pool(
267      ..pool,
268      stream: limited_stream([first.token], quota, tried, token),
269    )
270  rotation.stream(
271    pool,
272    first,
273    request(),
274    fn(_) { types.Continue },
275    rotation.Budget(8, 0, no_wait),
276    fn(_) { Nil },
277  )
278  |> should.equal(Error(types.HttpError(500, "sibling answered")))
279  let attempts = drain(tried, [])
280  attempts |> list.length |> should.equal(2)
281  list.first(attempts) |> should.equal(Ok(first.token))
282  // The session now sticks to the sibling until the quota resets.
283  let assert Ok(next) = pool.current()
284  { next.token == first.token } |> should.be_false
285  cleanup(root)
286}
287
288pub fn antigravity_capacity_waits_without_moving_test() -> Nil {
289  let #(root, _, home) = fixture()
290  let _ = write(home, "creds.json", google_accounts)
291  let tried = process.new_subject()
292  let paused = process.new_subject()
293  let pool =
294    antigravity.pool(
295      home,
296      "capacity-session",
297      limited_stream(["a1", "a2"], capacity, tried, token),
298    )
299  let assert Ok(first) = pool.current()
300  rotation.stream(
301    pool,
302    first,
303    request(),
304    fn(_) { types.Continue },
305    rotation.Budget(8, 0, fn(delay) {
306      process.send(paused, int.to_string(delay))
307    }),
308    fn(_) { Nil },
309  )
310  |> should.equal(Error(types.HttpError(429, capacity)))
311  // Capacity is the model's, not the account's: the same account each time.
312  drain(tried, []) |> list.unique |> should.equal([first.token])
313  drain(paused, []) |> should.equal(["4000", "8000", "15000", "30000"])
314  cleanup(root)
315}
316
317fn alibaba_config(a: String, b: String) -> String {
318  "{\"providers\":{\"ali-a\":{\"extension\":\"alibaba\",\"baseUrl\":\"http://127.0.0.1:1/a\",\"apiKey\":\""
319  <> a
320  <> "\",\"protocol\":\"chat_completions\"},\"ali-b\":{\"extension\":\"alibaba\",\"baseUrl\":\"http://127.0.0.1:1/b\",\"apiKey\":\""
321  <> b
322  <> "\",\"protocol\":\"chat_completions\"},\"other\":{\"extension\":\"openai\",\"baseUrl\":\"http://x\",\"apiKey\":\"not-alibaba\"}}}"
323}
324
325fn api_key(client: types.Client) -> String {
326  client.api_key
327}
328
329pub fn alibaba_rotates_to_another_profiles_key_test() -> Nil {
330  let #(root, _, home) = fixture()
331  let first_key = "ka-" <> int.to_string(unique())
332  let second_key = "kb-" <> int.to_string(unique())
333  let _ = write(home, "config.json", alibaba_config(first_key, second_key))
334  let tried = process.new_subject()
335  let body =
336    "{\"error\":{\"message\":\"Allocated quota exceeded, please increase your quota limit.\",\"code\":\"Throttling.AllocationQuota\"}}"
337  let pool = alibaba.pool(home, "ali-a", "ali-session")
338  let assert Ok(first) = pool.current()
339  first.api_key |> should.equal(first_key)
340  first.base_url |> should.equal("http://127.0.0.1:1/a")
341  rotation.stream(
342    rotation.Pool(
343      ..pool,
344      stream: limited_stream([first_key], body, tried, api_key),
345    ),
346    first,
347    openai_api.request("qwen3.8-max", [types.User("hi")]),
348    fn(_) { types.Continue },
349    rotation.Budget(8, 0, no_wait),
350    fn(_) { Nil },
351  )
352  |> should.equal(Error(types.HttpError(500, "sibling answered")))
353  drain(tried, []) |> should.equal([first_key, second_key])
354  // The limited key waits its turn; the sibling keeps its own base url.
355  let assert Ok(next) = pool.current()
356  next.api_key |> should.equal(second_key)
357  next.base_url |> should.equal("http://127.0.0.1:1/b")
358  cleanup(root)
359}
360
361fn google() -> antigravity.Endpoints {
362  antigravity.Endpoints("http://127.0.0.1:1", "http://127.0.0.1:1", "")
363}
364
365@external(erlang, "erlang", "unique_integer")
366fn unique() -> Int
367
368@external(erlang, "albedo_skills_test_support", "fixture")
369fn fixture() -> #(String, String, String)
370
371@external(erlang, "albedo_skills_test_support", "write")
372fn write(base: String, relative: String, content: String) -> String
373
374@external(erlang, "albedo_skills_test_support", "cleanup")
375fn cleanup(root: String) -> Nil
376
377pub fn account_order_keeps_selected_limited_and_tied_deadlines_distinct_test() -> Nil {
378  let entries = [
379    accounts.Entry("limited-selected", "a", True, 300),
380    accounts.Entry("open", "b", False, 0),
381    accounts.Entry("selected", "c", True, 0),
382    accounts.Entry("limited-first", "d", False, 200),
383    accounts.Entry("limited-second", "e", False, 200),
384  ]
385  accounts.order(entries, "session", Some("b"), 100)
386  |> should.equal([
387    "selected",
388    "open",
389    "limited-first",
390    "limited-second",
391    "limited-selected",
392  ])
393  accounts.order(entries, "session", Some("a"), 100)
394  |> should.equal([
395    "selected",
396    "open",
397    "limited-first",
398    "limited-second",
399    "limited-selected",
400  ])
401  accounts.order([], "session", None, 100) |> should.equal([])
402}
403
404pub fn account_spreading_preserves_fnv1a_byte_hash_test() -> Nil {
405  let entries =
406    list.map([0, 1, 2, 3, 4, 5, 6], fn(value) {
407      accounts.Entry(value, int.to_string(value), False, 0)
408    })
409  // FNV-1a over these bytes is 0x4F9F2CAB, including 32-bit overflow.
410  accounts.order(entries, "hello", None, 0)
411  |> should.equal([2, 3, 4, 5, 6, 0, 1])
412}
413
414@external(erlang, "albedo_accounts", "order")
415fn native_order(
416  scope: String,
417  values: List(Dict(String, Dynamic)),
418  session: String,
419  identity: fn(Dict(String, Dynamic)) -> String,
420) -> List(Dict(String, Dynamic))
421
422@external(erlang, "albedo_accounts", "remember")
423fn remember(scope: String, session: String, identity: String) -> Dynamic
424
425pub fn native_order_only_reads_identity_for_usable_sticky_accounts_test() -> Nil {
426  let account = dict.from_list([#("access", dynamic.string("unread token"))])
427  let no_identity = fn(_) {
428    panic as "this order does not need account identity"
429  }
430  native_order("identity-contract", [account], "unpinned", no_identity)
431  |> should.equal([account])
432  let _ = remember("identity-contract", "pinned", "chosen")
433  let limited =
434    dict.insert(
435      account,
436      "limitedUntil",
437      dynamic.int(clock.system_ms() + 1_000_000),
438    )
439  native_order("identity-contract", [limited], "pinned", no_identity)
440  |> should.equal([limited])
441}