@bongos/core 1.24.16 → 1.24.17

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/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.24.16",
6
- "core_contract": "1.24.16",
7
- "source_commit": "e8dbc5553383bc1a79bba1d5b54537e89ea25d12",
5
+ "core_version": "1.24.17",
6
+ "core_contract": "1.24.17",
7
+ "source_commit": "f7f9e7577824a2b7f8ddb83390d30f54eeec99a1",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-30T20:37:30.980Z",
9
+ "built_at": "2026-10-30T21:15:09.518Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 632,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 4463,
20
- "tree_sha256": "27ce12aa54042b2a48e3165cd44421d2ded0772dffbf4f47cdca60288614cc6a",
20
+ "tree_sha256": "64b322c3ab81616c72e8aaec297d36306593b337af36498181187eb6aa237648",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -352,7 +352,7 @@
352
352
  {
353
353
  "path": "clients/bongos-client/index.d.ts",
354
354
  "mode": "0000644",
355
- "sha256": "01c06f818ea2709ec5af984a362306ae444e04911f8062712b8f02ff2f8f036d"
355
+ "sha256": "9108d90c261aebdbb7e5077c0b6bd8eb678ac88d87a7c989bee01643ef0d8037"
356
356
  },
357
357
  {
358
358
  "path": "clients/bongos-client/index.mjs",
@@ -2462,7 +2462,7 @@
2462
2462
  {
2463
2463
  "path": "docs/api/openapi.json",
2464
2464
  "mode": "0000644",
2465
- "sha256": "8b51cc3fd37cd0cd371424ac821f84285f0e4fba5fc79a0efddb2ae92ca6bae4"
2465
+ "sha256": "2233f1eced2740296b71eb4bbe3539c6c359d757aa9185fb914c5dde9bcb3549"
2466
2466
  },
2467
2467
  {
2468
2468
  "path": "docs/architecture.md",
@@ -2487,12 +2487,12 @@
2487
2487
  {
2488
2488
  "path": "docs/copy-inventory.md",
2489
2489
  "mode": "0000644",
2490
- "sha256": "59713d4b3b53edf02ccd29125effd2e677c83ecc6a6616064be5b53ed3ff6ce1"
2490
+ "sha256": "e12823aa3c890a488a59b6e1fa098cbb527312c2d670b38f8aea1662bf6bbcd9"
2491
2491
  },
2492
2492
  {
2493
2493
  "path": "docs/copy-registry.json",
2494
2494
  "mode": "0000644",
2495
- "sha256": "f87a88a59f03b3b33c85a18b5f325ea976539b6ddaccc4e5c3745a2e423862aa"
2495
+ "sha256": "d72c6c9a078af32b7412c2ec8da6abe091d75251c2f0acd3c2dd7e44297e6198"
2496
2496
  },
2497
2497
  {
2498
2498
  "path": "docs/design/apex-pass-2-direction.md",
@@ -3002,7 +3002,7 @@
3002
3002
  {
3003
3003
  "path": "docs/module-api-changelog.md",
3004
3004
  "mode": "0000644",
3005
- "sha256": "691f8ca91b557183307f411c12394ec8d9b69dfa3217032fd8c8278183444fe8"
3005
+ "sha256": "1cd6c004501b1299126de68b3872cc35e7a015bba3362fc50b155d50db3632d9"
3006
3006
  },
3007
3007
  {
3008
3008
  "path": "docs/modules-contract.md",
@@ -3092,7 +3092,7 @@
3092
3092
  {
3093
3093
  "path": "docs/page-readings.json",
3094
3094
  "mode": "0000644",
3095
- "sha256": "41a0a25a8eab0ac5e0885ef4bace2ff10af9187493703b6cb5f72d2ea2d542a0"
3095
+ "sha256": "9470379d40e621c3a1dbb0bde781f96b3f13617f1ddec8a66202c7e0d5ef6ef7"
3096
3096
  },
3097
3097
  {
3098
3098
  "path": "docs/project-context.template.md",
@@ -4422,7 +4422,7 @@
4422
4422
  {
4423
4423
  "path": "modules/ai-studio/keys.js",
4424
4424
  "mode": "0000644",
4425
- "sha256": "a1b92a3661832537bedbbf5b5c8a111cef7398e928de71c6f631d5c4865f8b6c"
4425
+ "sha256": "4fd7a7b6f9bf0ed8f0e5c73ce24101832b5b82a4fb47a997bb58409be747fcc4"
4426
4426
  },
4427
4427
  {
4428
4428
  "path": "modules/ai-studio/migrations/ai_studio_001_keys.sql",
@@ -4462,7 +4462,7 @@
4462
4462
  {
4463
4463
  "path": "modules/ai-studio/routes/ai-studio.js",
4464
4464
  "mode": "0000644",
4465
- "sha256": "dbd4a55ba9942cdbc0103b402d1081423cad23adf30725d03b370aa73261c56f"
4465
+ "sha256": "4211cabb9b3923acc9d82bb0459efa6d2e2b4b115c9968927ec666a8115035ef"
4466
4466
  },
4467
4467
  {
4468
4468
  "path": "modules/ai-studio/studio.js",
@@ -5877,7 +5877,7 @@
5877
5877
  {
5878
5878
  "path": "modules/hall-ui/public/ai-studio-lib.js",
5879
5879
  "mode": "0000644",
5880
- "sha256": "903ee8f348ac461d2c2e2712e110d17ef6e1fa25d0f89a3ad570d456b482bdff"
5880
+ "sha256": "f845fd50a042a785b9ea06fb651ed55d459c2b7d631b19576199c6c641feed12"
5881
5881
  },
5882
5882
  {
5883
5883
  "path": "modules/hall-ui/public/ai-studio.css",
@@ -5892,7 +5892,7 @@
5892
5892
  {
5893
5893
  "path": "modules/hall-ui/public/ai-studio.js",
5894
5894
  "mode": "0000644",
5895
- "sha256": "70d9aee3ed9057f1460de3c8ff641396b28271186a7a980e1785061a675dbe4e"
5895
+ "sha256": "67c9b6f4652a71357fc1f435637f14ed6725530c0544ca338a7b95e128bd7741"
5896
5896
  },
5897
5897
  {
5898
5898
  "path": "modules/hall-ui/public/ai-studio.states.json",
@@ -11692,12 +11692,12 @@
11692
11692
  {
11693
11693
  "path": "package-lock.json",
11694
11694
  "mode": "0000644",
11695
- "sha256": "2fdb487bbe1ef223f624f1aada21af2df70d420640eb77b4b61ce7995ad95424"
11695
+ "sha256": "53297fe3ccc01d30cac66baffc1c3ddc528b91d3bf36d6c626e97477405a6216"
11696
11696
  },
11697
11697
  {
11698
11698
  "path": "package.json",
11699
11699
  "mode": "0000644",
11700
- "sha256": "93b9f649209897afd37554864ecd9f8372414a168191d11e3c8e9f14296cb593"
11700
+ "sha256": "f8bc5d960904c6118c3ff73f3181bc1ae4761abccba9f1df71719908ccf12a65"
11701
11701
  },
11702
11702
  {
11703
11703
  "path": "public-docs/index.html",
@@ -11717,7 +11717,7 @@
11717
11717
  {
11718
11718
  "path": "release-notes.json",
11719
11719
  "mode": "0000644",
11720
- "sha256": "f01ac97b1d814ccd53835a41a5a3a5feb2c091b8673f36b26b9138935c0009e5"
11720
+ "sha256": "33bb0faf10fbdb3afcf054c4ab3be42ae54139ee69f7df8b84340a6d3318466b"
11721
11721
  },
11722
11722
  {
11723
11723
  "path": "scripts/bongos/CLAUDE.md",
@@ -15582,7 +15582,7 @@
15582
15582
  {
15583
15583
  "path": "src/module-api.js",
15584
15584
  "mode": "0000644",
15585
- "sha256": "ac2f7155740717b05c1024991a25fd662afedd122eb15672126e2a75d82eb423"
15585
+ "sha256": "06f9627d1fd34e3166cb8f6a16db6257307eec862791c77d5e17568dcf83e156"
15586
15586
  },
15587
15587
  {
15588
15588
  "path": "src/module-loader/catalog.js",
@@ -15852,7 +15852,7 @@
15852
15852
  {
15853
15853
  "path": "tests/ai_studio_generate.mjs",
15854
15854
  "mode": "0000644",
15855
- "sha256": "ee75867d1f3a45d0a06242b664f2d2f0829f34c586bcd883c04ac64eefc53dfd"
15855
+ "sha256": "e4b659932669bd58e44a0216a5b1e8643a0961a60ecda9b695f9042b40fc3c4d"
15856
15856
  },
15857
15857
  {
15858
15858
  "path": "tests/ai_studio_keys.mjs",
@@ -18292,7 +18292,7 @@
18292
18292
  {
18293
18293
  "path": "tests/hall_ai_studio.mjs",
18294
18294
  "mode": "0000644",
18295
- "sha256": "5b027b4922f78764d8d1222763e4aca31cc2d43895f616e37b35d018ca5188a2"
18295
+ "sha256": "e4c7673b57e03ce15eade36932ac02efdceb06101c53f673d8978e35de22cb33"
18296
18296
  },
18297
18297
  {
18298
18298
  "path": "tests/hall_approval_queue.mjs",
@@ -303,7 +303,7 @@ export interface PostAgentsNameEnableResponse { ok: boolean; agent: unknown }
303
303
  export interface PostAgentsNameInvokeRequest { input?: string }
304
304
  export interface PostAgentsNameInvokeResponse { ok: boolean; run_id: unknown; poll: unknown; message: unknown }
305
305
  export interface PostAgentsResponse { ok: boolean; agent: unknown }
306
- export interface PostAiStudioRequestsRequest { prompt: string; file_ids: unknown[]; pay_with?: "own" | "ours" }
306
+ export interface PostAiStudioRequestsRequest { prompt: string; file_ids: unknown[]; pay_with?: "own" | "ours" | "openrouter" | "gemini" }
307
307
  export interface PostAiStudioRequestsResponse { request: unknown }
308
308
  export interface PostAiStudioTopupRequest { amount_usd: number }
309
309
  export interface PostAiStudioTopupResponse { topup: unknown; balance: unknown }
@@ -34768,7 +34768,9 @@
34768
34768
  "type": "string",
34769
34769
  "enum": [
34770
34770
  "own",
34771
- "ours"
34771
+ "ours",
34772
+ "openrouter",
34773
+ "gemini"
34772
34774
  ]
34773
34775
  }
34774
34776
  },
@@ -4,13 +4,13 @@
4
4
  > Deterministic on a given tree — regenerate and commit rather than hand-editing.
5
5
  > Machine-readable twin: [`copy-registry.json`](copy-registry.json).
6
6
 
7
- **3278 strings** across 3 surfaces · 2595 distinct pieces of copy · 3278 certain, 0 heuristic · 275 shown in more than one place.
7
+ **3276 strings** across 3 surfaces · 2593 distinct pieces of copy · 3276 certain, 0 heuristic · 275 shown in more than one place.
8
8
 
9
9
  ## Surfaces
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 172 | 2165 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 172 | 2163 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 91 |
15
15
  | `landing` | `modules/public-landing/public` | 10 | 1022 |
16
16
 
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `72a29648399b` | 4x | Tweak a page | modules/hall-ui/public/tweak-editor.html:32, modules/hall-ui/public/tweak-editor.html:32, modules/hall-ui/public/tweak-editor.html:55 |
79
79
  | … | | _235 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 2165 strings
81
+ ## `builders-hall` — 2163 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -581,7 +581,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
581
581
  | `94c68a78cd89` | certain | Follow a route from a goal you know to reach it — or turn explore off to see everything. | `modules/hall-ui/public/sky-interaction.js:35` |
582
582
  | `d5d7bad187ac` | certain | For a criterion with nothing to see on screen: it will be signed off without a recording | `modules/hall-ui/public/criterion-uat-ui.js:88` |
583
583
  | `3001bd688837` | certain | Founding builders | `modules/hall-ui/public/ranks.html:40` |
584
- | `e9a1af5dc515` | certain | Free here. {…} bills you for it directly. | `modules/hall-ui/public/ai-studio.js:334` |
584
+ | `e9a1af5dc515` | certain | Free here. {…} bills you for it directly. | `modules/hall-ui/public/ai-studio.js:336` |
585
585
  | `36ecb5d3686c` | certain | Fresh CLI token issued. Paste this into a terminal in your project folder: | `modules/hall-ui/public/settings.js:800` |
586
586
  | `55c71d76a94c` | certain | From creation to production — and the exact step where {{currency}} are awarded (at | `modules/hall-ui/public/diagrams.html:49` |
587
587
  | `edced5d528c3` | certain | From the live site | `modules/hall-ui/public/studio.html:155` |
@@ -860,7 +860,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
860
860
  | `b5839a34590c` | certain | My agents — {{worldName}} | `modules/hall-ui/public/my-agents.html:6` |
861
861
  | `b5839a34590c` | certain | My agents — {{worldName}} | `modules/hall-ui/public/my-agents.html:6` |
862
862
  | `39afdc03594a` | certain | My balance | `modules/hall-ui/public/ai-studio.js:340` |
863
- | `b7d4eeb3f762` | certain | My own key | `modules/hall-ui/public/ai-studio.js:334` |
864
863
  | `3d0d554cd684` | certain | Name (optional) | `modules/hall-ui/public/gate.js:201` |
865
864
  | `ae84ebe1d735` | certain | Name cannot be blank. | `modules/hall-ui/public/settings.js:649` |
866
865
  | `31e91ee6ce03` | certain | Name must be {…} characters or fewer. | `modules/hall-ui/public/settings.js:654` |
@@ -2137,7 +2136,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
2137
2136
  | `cf89ac85440d` | certain | {…} earned | `modules/hall-ui/public/goals-render.js:821` |
2138
2137
  | `55b949873848` | certain | {…} is in. | `modules/hall-ui/public/tweak-editor.js:578` |
2139
2138
  | `80b4aa799a4b` | certain | {…} is writing this page now. You can read it, download it, or take another page from the studio. | `modules/hall-ui/public/tweak-editor.js:329` |
2140
- | `c80c3434ae36` | certain | {…} key ending {…} | `modules/hall-ui/public/ai-studio.js:334` |
2141
2139
  | `5ad81ff61920` | certain | {…} lands in your balance; your card is charged {…}. | `modules/hall-ui/public/ai-studio.js:579` |
2142
2140
  | `92b79edf21f6` | certain | {…} left | `modules/hall-ui/public/market-watch.js:73` |
2143
2141
  | `830d2be6f859` | certain | {…} line | `modules/hall-ui/public/tweak-editor.js:235` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/bongos/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 3278,
7
- "unique_ids": 2595,
8
- "certain": 3278,
6
+ "strings": 3276,
7
+ "unique_ids": 2593,
8
+ "certain": 3276,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 275
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 172,
16
- "strings": 2165
16
+ "strings": 2163
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -7694,7 +7694,7 @@
7694
7694
  "surface": "builders-hall",
7695
7695
  "text": "Free here. {…} bills you for it directly.",
7696
7696
  "file": "modules/hall-ui/public/ai-studio.js",
7697
- "line": 334,
7697
+ "line": 336,
7698
7698
  "origin": "js-markup",
7699
7699
  "confidence": "certain"
7700
7700
  },
@@ -10200,15 +10200,6 @@
10200
10200
  "origin": "js-markup",
10201
10201
  "confidence": "certain"
10202
10202
  },
10203
- {
10204
- "id": "b7d4eeb3f762",
10205
- "surface": "builders-hall",
10206
- "text": "My own key",
10207
- "file": "modules/hall-ui/public/ai-studio.js",
10208
- "line": 334,
10209
- "origin": "js-markup",
10210
- "confidence": "certain"
10211
- },
10212
10203
  {
10213
10204
  "id": "3d0d554cd684",
10214
10205
  "surface": "builders-hall",
@@ -21693,15 +21684,6 @@
21693
21684
  "origin": "js-markup",
21694
21685
  "confidence": "certain"
21695
21686
  },
21696
- {
21697
- "id": "c80c3434ae36",
21698
- "surface": "builders-hall",
21699
- "text": "{…} key ending {…}",
21700
- "file": "modules/hall-ui/public/ai-studio.js",
21701
- "line": 334,
21702
- "origin": "js-markup",
21703
- "confidence": "certain"
21704
- },
21705
21687
  {
21706
21688
  "id": "5ad81ff61920",
21707
21689
  "surface": "builders-hall",
@@ -3288,5 +3288,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3288
3288
  landed since 1.24.13 with no explicit bump. run 41110951037. (task 1002620)
3289
3289
  1.24.16 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3290
3290
  landed since 1.24.15 with no explicit bump. run 41124515103. (task 1002620)
3291
+ 1.24.17 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3292
+ landed since 1.24.16 with no explicit bump. run 41129472650. (task 1002620)
3291
3293
  ---------------------------------------------------------------------------
3292
3294
  ```
@@ -5,16 +5,16 @@
5
5
  "adr": "0341 D3",
6
6
  "reread": "node scripts/bongos/page-reader.js --stale",
7
7
  "how": ["Generated by node scripts/bongos/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
8
- "counts": {"pages":52,"lines":5612,"placed":1755,"shared":461,"unplaced":3396,"omitted_data":3766},
8
+ "counts": {"pages":52,"lines":5612,"placed":1753,"shared":461,"unplaced":3398,"omitted_data":3766},
9
9
  "pages": [
10
10
  {
11
11
  "id": "builders:ai-studio",
12
12
  "surface": "builders",
13
13
  "title": "Studio",
14
14
  "states_read": ["pick","describe","make","waiting","result","result-own","failed","balance","balance-all","out"],
15
- "files_hash": "b4dc755790ac158e",
16
- "reading_hash": "7fa800b814783e81",
17
- "counts": {"lines":102,"placed":40,"shared":8,"unplaced":54},
15
+ "files_hash": "48c0f2c2c104a63b",
16
+ "reading_hash": "475acccd2681bd7f",
17
+ "counts": {"lines":102,"placed":38,"shared":8,"unplaced":56},
18
18
  "omitted": {"data":15},
19
19
  "notes": [],
20
20
  "lines": [
@@ -84,9 +84,9 @@
84
84
  {"key":"L0064","section":"How do you want to pay?","text":"PDF","placement":"unplaced","state":"make"},
85
85
  {"key":"L0065","section":"How do you want to pay?","text":"“Turn the sketch into a finished hero picture for the landing pa…”","placement":"unplaced","state":"make"},
86
86
  {"key":"L0066","section":"How do you want to pay?","text":"change","placement":"unplaced","state":"make"},
87
- {"key":"L0067","section":"How do you want to pay?","text":"My own key","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":334,"string_id":"b7d4eeb3f762","state":"make"},
88
- {"key":"L0068","section":"How do you want to pay?","text":"Free here. {…} bills you for it directly.","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":334,"string_id":"e9a1af5dc515","state":"make"},
89
- {"key":"L0069","section":"How do you want to pay?","text":"{…} key ending {…}","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":334,"string_id":"c80c3434ae36","state":"make"},
87
+ {"key":"L0067","section":"How do you want to pay?","text":"My own key","placement":"unplaced","state":"make"},
88
+ {"key":"L0068","section":"How do you want to pay?","text":"Free here. {…} bills you for it directly.","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":336,"string_id":"e9a1af5dc515","state":"make"},
89
+ {"key":"L0069","section":"How do you want to pay?","text":"Gemini key ending {…}","placement":"unplaced","state":"make"},
90
90
  {"key":"L0070","section":"How do you want to pay?","text":"My balance","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":340,"string_id":"39afdc03594a","state":"make"},
91
91
  {"key":"L0071","section":"How do you want to pay?","text":"Usually a few cents. Up to {…} is set aside, and the rest comes back.","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":340,"string_id":"664555b65658","state":"make"},
92
92
  {"key":"L0072","section":"How do you want to pay?","text":"{…} left","placement":"placed","file":"modules/hall-ui/public/market-watch.js","line":73,"string_id":"92b79edf21f6","state":"make"},
@@ -4216,7 +4216,7 @@
4216
4216
  "surface": "builders",
4217
4217
  "title": "Studio",
4218
4218
  "states_read": ["home","out","flow","flow-pick","status","words-offer","words-edit","reviews","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back","approvals-page"],
4219
- "files_hash": "9eda81ed0ffd5014",
4219
+ "files_hash": "6c591072e20de9f0",
4220
4220
  "reading_hash": "bcba11e6cef56218",
4221
4221
  "counts": {"lines":31,"placed":16,"shared":6,"unplaced":9},
4222
4222
  "omitted": {"data":4},
@@ -65,6 +65,10 @@ function listPublic(rows) {
65
65
  function chooseKey(stored, payWith) {
66
66
  const own = ['openrouter', 'gemini'].find((p) => (stored || []).includes(p));
67
67
  if (payWith === 'ours') return { source: 'ours' };
68
+ // A named provider picks that key when both are saved.
69
+ if (isProvider(payWith)) {
70
+ return (stored || []).includes(payWith) ? { source: 'own', provider: payWith } : { error: 'no_own_key' };
71
+ }
68
72
  if (payWith === 'own' && !own) return { error: 'no_own_key' };
69
73
  if (payWith !== undefined && payWith !== null && payWith !== 'own') return { error: 'bad_pay_with' };
70
74
  return own ? { source: 'own', provider: own } : { source: 'ours' };
@@ -173,7 +173,7 @@ module.exports = function buildAiStudioRouter() {
173
173
  if (api.validateOrRespond(req, res, {
174
174
  prompt: { required: true, type: 'string', maxLength: S.MAX_PROMPT + 200 },
175
175
  file_ids: { required: true, type: 'array', maxItems: S.MAX_FILES },
176
- pay_with: { type: 'string', enum: ['own', 'ours'] },
176
+ pay_with: { type: 'string', enum: ['own', 'ours', 'openrouter', 'gemini'] },
177
177
  })) return;
178
178
  if (!S.isAdult(await db.latestAgeTier(req.builder.id))) return res.fail('adults_only', { status: 403, message: ADULTS_ONLY });
179
179
  const checked = S.checkRequest(req.body);
@@ -107,25 +107,31 @@
107
107
 
108
108
  // The own key the server will run on: OpenRouter first, as keys.chooseKey
109
109
  // prefers it (it reports its cost). null when none is saved.
110
- function ownKey(keys) {
110
+ function ownKey(keys) { return ownKeys(keys)[0] || null; }
111
+
112
+ // Every own key that is saved, OpenRouter first: step 3 offers one card each,
113
+ // so a builder with both picks which one runs.
114
+ function ownKeys(keys) {
111
115
  const list = (keys && keys.keys) || [];
112
- const set = (p) => list.find((k) => k && k.provider === p && k.set);
113
- const k = set('openrouter') || set('gemini');
114
- if (!k) return null;
115
- return k.provider === 'openrouter'
116
- ? { provider: 'openrouter', name: 'OpenRouter', billedBy: 'OpenRouter', last4: k.last4 }
117
- : { provider: 'gemini', name: 'Gemini', billedBy: 'Google', last4: k.last4 };
116
+ return ['openrouter', 'gemini']
117
+ .map((p) => list.find((k) => k && k.provider === p && k.set))
118
+ .filter(Boolean)
119
+ .map((k) => (k.provider === 'openrouter'
120
+ ? { provider: 'openrouter', name: 'OpenRouter', billedBy: 'OpenRouter', last4: k.last4 }
121
+ : { provider: 'gemini', name: 'Gemini', billedBy: 'Google', last4: k.last4 }));
118
122
  }
119
123
 
120
- // Step 3's two cards. balance is GET /ai-studio/balance's body.
124
+ // Step 3's cards. balance is GET /ai-studio/balance's body.
121
125
  function payChoices(keys, balance) {
122
- const own = ownKey(keys);
126
+ const owns = ownKeys(keys);
127
+ const own = owns[0] || null;
123
128
  const b = balance || {};
124
129
  const avail = b.balance ? b.balance.available_micro : null;
125
130
  const max = isMicro(b.max_cost_micro) ? b.max_cost_micro : null;
126
131
  const payments = !!b.balance;
127
132
  return {
128
133
  own,
134
+ owns,
129
135
  payments,
130
136
  setAside: max === null ? '' : usd(max),
131
137
  left: payments ? `${balanceUsd(avail)} left` : '',
@@ -260,7 +266,7 @@
260
266
 
261
267
  const lib = {
262
268
  MAX_FILES, MAX_IMAGE_BYTES, MAX_PDF_BYTES, IMAGE_TYPES, PDF_TYPE, STEPS, TOPUP_CHOICES, ANSWER_AFTER_SEC,
263
- usd, balanceUsd, sizeText, routeOf, checkFile, isImage, stepMarks, quoteLine, ownKey, payChoices,
269
+ usd, balanceUsd, sizeText, routeOf, checkFile, isImage, stepMarks, quoteLine, ownKey, ownKeys, payChoices,
264
270
  waitSteps, clock, isFinished, receipt, againLine, monthSummary, ledgerLine, replyHtml, homeLine,
265
271
  };
266
272
  if (typeof module !== 'undefined' && module.exports) module.exports = lib;
@@ -331,12 +331,12 @@
331
331
  const files = readyFiles();
332
332
  const c = lib.payChoices(state.keys, state.balance);
333
333
  const thumbs = files.slice(0, 3).map((f) => thumb(f)).join('');
334
- const ownCard = c.own ? `
335
- <button type="button" class="st-action ais-card ais-card--own" data-pay="own">
336
- <span class="st-action__label">My own key</span>
337
- <span class="ais-card__line">Free here. ${esc(c.own.billedBy)} bills you for it directly.</span>
338
- <span class="ais-card__foot">${esc(c.own.name)} key ending ${esc(c.own.last4 || '')}</span>
339
- </button>` : '';
334
+ const ownCard = c.owns.map((k) => `
335
+ <button type="button" class="st-action ais-card ais-card--own" data-pay="${esc(k.provider)}">
336
+ <span class="st-action__label">${c.owns.length > 1 ? `My ${esc(k.name)} key` : 'My own key'}</span>
337
+ <span class="ais-card__line">Free here. ${esc(k.billedBy)} bills you for it directly.</span>
338
+ <span class="ais-card__foot">${c.owns.length > 1 ? '' : `${esc(k.name)} `}key ending ${esc(k.last4 || '')}</span>
339
+ </button>`).join('');
340
340
  const balanceCard = c.payments ? `
341
341
  <button type="button" class="st-action ais-card ais-card--ours" data-pay="ours" ${c.short ? 'disabled' : ''}>
342
342
  <span class="st-action__label">My balance</span>
@@ -346,8 +346,8 @@
346
346
  let foot = 'You only pay for what it really cost. If it fails, nothing is taken.';
347
347
  let extra = '';
348
348
  if (c.payments && c.short) extra = `<p class="ais-faint">Your balance is under ${esc(c.setAside)}. <a href="/builders/ai-studio?view=balance">Add money</a> to pay this way.</p>`;
349
- if (!c.own) extra += `<p class="ais-faint">Have your own OpenRouter or Gemini key? <a href="${SETTINGS_KEYS}">Add it in Settings</a> and make things for free here.</p>`;
350
- if (!c.own && !c.payments) foot = 'Paying from a balance is not set up here yet, so add your own key in Settings to make something.';
349
+ if (!c.owns.length) extra += `<p class="ais-faint">Have your own OpenRouter or Gemini key? <a href="${SETTINGS_KEYS}">Add it in Settings</a> and make things for free here.</p>`;
350
+ if (!c.owns.length && !c.payments) foot = 'Paying from a balance is not set up here yet, so add your own key in Settings to make something.';
351
351
  page.innerHTML = `${stepHead('make')}
352
352
  <h1 class="ais-ask" data-head tabindex="-1">How do you want to pay?</h1>
353
353
  <div class="ais-row ais-row--summary">${thumbs}<span class="ais-grow ais-soft">${esc(lib.quoteLine(state.draft.prompt))}</span><a class="ais-link" href="/builders/ai-studio?step=describe" data-nav="step=describe">change</a></div>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.16",
3
+ "version": "1.24.17",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.16",
9
+ "version": "1.24.17",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.16",
3
+ "version": "1.24.17",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -10981,5 +10981,11 @@
10981
10981
  "id": "1005028",
10982
10982
  "text": "Settings now has a Your age section where you record your age group, which switches on AI Studio for adults."
10983
10983
  }
10984
+ ],
10985
+ "1.24.17": [
10986
+ {
10987
+ "id": "1005029",
10988
+ "text": "Artists with both an OpenRouter and a Gemini key saved can now choose which one runs their AI request."
10989
+ }
10984
10990
  ]
10985
10991
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.16'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.17'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -246,6 +246,8 @@ test('which key runs: theirs whenever they have one, unless they ask for ours',
246
246
  assert.deepEqual(K.chooseKey(['openrouter'], 'ours'), { source: 'ours' });
247
247
  assert.deepEqual(K.chooseKey([], 'own'), { error: 'no_own_key' });
248
248
  assert.deepEqual(K.chooseKey(['openrouter'], 'free'), { error: 'bad_pay_with' });
249
+ assert.deepEqual(K.chooseKey(['gemini', 'openrouter'], 'gemini'), { source: 'own', provider: 'gemini' }, 'a named key wins over the default');
250
+ assert.deepEqual(K.chooseKey(['openrouter'], 'gemini'), { error: 'no_own_key' });
249
251
  });
250
252
 
251
253
  test('on their own key no money moves: one own_key row, price 0, cost noted', async () => {
@@ -469,3 +469,18 @@ test('walls: the studio home shows the AI door only where the module is on', ()
469
469
  assert.match(js, /mods\.enabled\['ai-studio'\] !== true\) return;/, 'off by default: a missing flag hides the door');
470
470
  assert.match(js, /loadAi\(\)\]/);
471
471
  });
472
+
473
+ test('page: with both own keys saved, step 3 offers one card each and sends the one pressed', async () => {
474
+ const both = { keys: [{ provider: 'openrouter', set: true, last4: '4f2a' }, { provider: 'gemini', set: true, last4: '9c1d' }] };
475
+ assert.deepEqual(lib.ownKeys(both).map((k) => k.provider), ['openrouter', 'gemini']);
476
+ const p = bootPage('?step=make&from=1001', { routes: { 'GET /api/bongos/ai-studio/keys': both } });
477
+ await settle();
478
+ const pays = p.page.querySelectorAll('[data-pay]').map((b) => b.getAttribute('data-pay'));
479
+ assert.deepEqual(pays, ['openrouter', 'gemini', 'ours']);
480
+ assert.match(p.page.innerHTML, /My Gemini key/);
481
+ const gem = p.page.querySelectorAll('[data-pay]').find((b) => b.getAttribute('data-pay') === 'gemini');
482
+ await gem.fire('click');
483
+ await settle();
484
+ const sent = p.calls.find((c) => c.method === 'POST' && c.url.endsWith('/ai-studio/requests'));
485
+ assert.equal(sent.body.pay_with, 'gemini');
486
+ });