opencode-navigator 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,13 +117,16 @@ child?.kill()
117
117
  }}}
118
118
  function normalizeProviderQuota(snapshot){if(!snapshot.providerId||!snapshot.providerName||!snapshot.model.providerID||!snapshot.model.modelID||!snapshot.model.sessionID||!Number.isFinite(snapshot.fetchedAt)){throw new Error("Invalid provider quota identity")
119
119
  }
120
- const windows=snapshot.windows.map((window)=>{if(!window.id||!window.label||window.modelID!==snapshot.model.modelID||!window.unit||!Number.isFinite(window.used)||window.total!==undefined&&(!Number.isFinite(window.total)||window.total<0)||window.durationMinutes!==undefined&&(!Number.isFinite(window.durationMinutes)||window.durationMinutes<0)||window.resetsAt!==undefined&&!Number.isFinite(window.resetsAt)){throw new Error("Quota window does not match the selected model or contains invalid values")
120
+ const windows=snapshot.windows.map((window)=>{if(!window.id||!window.label||window.modelID!==snapshot.model.modelID||!window.unit||window.used!==undefined&&!Number.isFinite(window.used)||window.remaining!==undefined&&!Number.isFinite(window.remaining)||window.used===undefined&&window.remaining===undefined&&window.total===undefined||window.total!==undefined&&(!Number.isFinite(window.total)||window.total<0)||window.durationMinutes!==undefined&&(!Number.isFinite(window.durationMinutes)||window.durationMinutes<0)||window.resetsAt!==undefined&&!Number.isFinite(window.resetsAt)){throw new Error("Quota window does not match the selected model or contains invalid values")
121
121
  }
122
122
  return{...window}})
123
123
  const bankedResets=snapshot.bankedResets
124
+ const balances=snapshot.balances?.map((balance)=>{if(!balance.id||!balance.label||balance.scope!=="account"&&balance.scope!=="key"||balance.amount===undefined&&!balance.unlimited||balance.amount!==undefined&&(typeof balance.amount!=="string"||!/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(balance.amount)||!Number.isFinite(Number(balance.amount)))||balance.unit!==undefined&&(typeof balance.unit!=="string"||!balance.unit))
125
+ throw new Error("Invalid provider balance")
126
+ return{...balance}})
124
127
  if(bankedResets&&(!Number.isSafeInteger(bankedResets.availableCount)||bankedResets.availableCount<0)){throw new Error("Invalid banked reset count")
125
128
  }
126
- return{...snapshot,windows,...bankedResets&&{bankedResets:{availableCount:bankedResets.availableCount,...bankedResets.credits&&{credits:[...bankedResets.credits]}}}}}
129
+ return{...snapshot,windows,...balances&&{balances},...bankedResets&&{bankedResets:{availableCount:bankedResets.availableCount,...bankedResets.credits&&{credits:[...bankedResets.credits]}}}}}
127
130
  import {createSignal as createSignal2,untrack} from "solid-js"
128
131
  import {createSignal} from "solid-js"
129
132
  var idle={status:"idle"}
@@ -190,7 +193,7 @@ setStates((values)=>({...values,[target]:{status:"error",error:requestError(caus
190
193
  },finish
191
194
  }}
192
195
  return{state,start,abort,abortAll}}
193
- function createProviderLimitsController(adapters,selectedModel,signal){const requests=createRequestState(signal)
196
+ function createProviderLimitsController(adapters,selectedModel,signal,refreshContext){const requests=createRequestState(signal)
194
197
  const [snapshots,setSnapshots]=createSignal2({})
195
198
  const targetKey=(model)=>JSON.stringify(model)
196
199
  let activeKey
@@ -204,14 +207,16 @@ targetAbort=new AbortController
204
207
  activeKey=key
205
208
  }
206
209
  return{model,adapter}}
207
- async function refresh(force=false){const{model,adapter}=current()
210
+ async function refresh(force=false){if(force&&refreshContext)
211
+ await refreshContext()
212
+ const{model,adapter}=current()
208
213
  if(!model||!adapter)
209
214
  return
210
215
  const key=targetKey(model)
211
216
  const request=requests.start(key,"read provider limits",Boolean(snapshots()[key]),force)
212
217
  if(!request)
213
218
  return
214
- try{const snapshot=normalizeProviderQuota(await adapter.read(model,request.signal))
219
+ try{const snapshot=normalizeProviderQuota(await adapter.read(model,request.signal,force))
215
220
  if(!request.isCurrent()||targetKey(current().model??model)!==key)
216
221
  return
217
222
  setSnapshots((value)=>({...value,[key]:snapshot}))
@@ -238,7 +243,7 @@ function windows(limit,model){return ["primary","secondary"].flatMap((name)=>{co
238
243
  return value?[{id:`${limit.limitId ?? model.modelID}.${name}`,label:limit.limitName?`${limit.limitName} \xB7 ${name}`:name,limitID:limit.limitId??undefined,modelID:model.modelID,unit:"%",used:value.usedPercent,...value.windowDurationMins!==null&&{durationMinutes:value.windowDurationMins},...value.resetsAt!==null&&{resetsAt:value.resetsAt}}]:[]
239
244
  })
240
245
  }
241
- function createCodexQuotaAdapter(client){const adapter={id:"codex",name:"Codex CLI",supports:(model)=>model.providerID==="openai"&&Boolean(model.accountID),async discoverAccount(signal){const account=await client.request("account/read",{refreshToken:false},signal)
246
+ function createCodexQuotaAdapter(client,validateHost){const adapter={id:"codex",name:"Codex CLI",supports:(model)=>(model.providerID==="openai"||model.hostConnection?.providerID==="openai")&&model.hostConnection?.method!=="api"&&Boolean(model.accountID)&&(!model.hostConnection||model.hostConnection.status==="ready"),...validateHost&&{validateHost},async discoverAccount(signal){const account=await client.request("account/read",{refreshToken:false},signal)
242
247
  if(account.account?.type!=="chatgpt")
243
248
  throw new Error("Sign in to Codex CLI with ChatGPT")
244
249
  const usage=await client.request("account/rateLimits/read",{supportsLunaReserve:false,excludeResetCreditDetails:true},signal)
@@ -247,6 +252,8 @@ throw new Error("Codex account identity is unavailable")
247
252
  return usage.accountId
248
253
  },async read(model,signal){const base={model,providerId:"codex",providerName:"Codex CLI",fetchedAt:Date.now(),windows:[]
249
254
  }
255
+ if(validateHost&&!await validateHost(model,signal))
256
+ return{...base,availability:"unauthenticated"}
250
257
  const account=await client.request("account/read",{refreshToken:false},signal)
251
258
  if(account.account?.type!=="chatgpt")
252
259
  return{...base,availability:"unauthenticated"}
@@ -280,6 +287,9 @@ throw new Error("Codex account changed")
280
287
  } else
281
288
  await adapter.prepareResetCreditConsumption(model,attempt.creditID,signal)
282
289
  signal.throwIfAborted()
290
+ if(validateHost&&!await validateHost(model,signal))
291
+ throw new Error("OpenCode account or connection changed")
292
+ signal.throwIfAborted()
283
293
  const result=await client.request("account/rateLimitResetCredit/consume",{idempotencyKey:attempt.idempotencyKey,...attempt.creditID&&{creditId:attempt.creditID}},signal)
284
294
  return result.outcome
285
295
  },dispose:client.dispose
@@ -291,21 +301,43 @@ import {createSignal as createSignal3} from "solid-js"
291
301
  import {GuardedConfirmation} from "./tui.js"
292
302
  function createCodexAccountLink(input){const [busy,setBusy]=createSignal3(false)
293
303
  async function link(){const selected=input.model()
294
- if(busy()||!selected||selected.providerID!=="openai"||!input.adapter.discoverAccount)
304
+ if(busy()||!selected||selected.providerID!=="openai"&&selected.hostConnection?.providerID!=="openai"||!input.adapter.discoverAccount)
305
+ return
306
+ if(selected.hostConnection&&selected.hostConnection.status!=="ready")
295
307
  return
296
308
  const key=JSON.stringify(selected)
297
309
  const current=()=>JSON.stringify(input.model())===key
310
+ const signal=input.api.lifecycle?.signal??new AbortController().signal
311
+ const validHost=async ()=>!input.adapter.validateHost||await input.adapter.validateHost(selected,signal)
298
312
  setBusy(true)
299
- try{const accountID=await input.adapter.discoverAccount(input.api.lifecycle?.signal??new AbortController().signal)
313
+ try{if(!await validHost()||!current())
314
+ return
315
+ const accountID=await input.adapter.discoverAccount(signal)
300
316
  if(!current())
301
317
  return
302
- input.dialogs.open(()=>_$createComponent(GuardedConfirmation,{get api(){return input.api
303
- },current,title:"Link Codex CLI account",get message(){return `Use Codex CLI account ${accountID} for ${selected.modelID}? Only link accounts you know match this OpenCode provider.`
304
- },onConfirm:()=>{if(!current())
318
+ if(selected.hostConnection?.accountID&&selected.hostConnection.accountID!==accountID){input.api.ui.toast({variant:"warning",title:"Codex account",message:"Sign in to Codex CLI with the account selected in OpenCode."
319
+ })
320
+ return
321
+ }
322
+ async function confirm(){if(busy()||!current())
323
+ return
324
+ setBusy(true)
325
+ try{if(!await validHost()||!current())
326
+ return
327
+ const latest=await input.adapter.discoverAccount(signal)
328
+ if(!current()||latest!==accountID)
305
329
  return
306
- input.preferences.setCodexAccountBinding(selected.providerID,selected.modelID,accountID)
330
+ input.preferences.setCodexConnectionBinding(selected.providerID,selected.hostConnection?.id,accountID)
307
331
  input.dialogs.back()
308
- }}))
332
+ } catch{if(current())
333
+ input.api.ui.toast({variant:"warning",title:"Codex account",message:"Account verification failed; try again."
334
+ })
335
+ } finally{setBusy(false)
336
+ }}
337
+ input.dialogs.open(()=>_$createComponent(GuardedConfirmation,{get api(){return input.api
338
+ },current,title:"Link Codex CLI account",get message(){return `Use Codex CLI account ${accountID} for ${selected.providerID}${selected.hostConnection?.id ? " connection" : ""} across models? Only confirm matching accounts.`
339
+ },onConfirm:()=>void confirm()
340
+ }))
309
341
  } catch{if(current())
310
342
  input.api.ui.toast({variant:"warning",title:"Codex account",message:"Install a supported Codex CLI and sign in with ChatGPT, then try again."
311
343
  })
@@ -314,35 +346,41 @@ input.api.ui.toast({variant:"warning",title:"Codex account",message:"Install a s
314
346
  return{link,busy
315
347
  }}
316
348
  import {effect as _$effect} from "@opentui/solid"
317
- import {createTextNode as _$createTextNode} from "@opentui/solid"
318
349
  import {insertNode as _$insertNode} from "@opentui/solid"
319
350
  import {insert as _$insert} from "@opentui/solid"
320
351
  import {memo as _$memo} from "@opentui/solid"
321
352
  import {setProp as _$setProp} from "@opentui/solid"
322
353
  import {createElement as _$createElement} from "@opentui/solid"
323
- function QuotaWindowRow(props){const reset=()=>props.window.resetsAt===undefined?"":` \xB7 resets ${new Date(props.window.resetsAt * 1000).toLocaleString()}`
324
- return(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("text"),_el$3=_$createTextNode(`: `),_el$5=_$createTextNode(` used`),_el$6=_$createElement("text")
354
+ function QuotaWindowRow(props){const kind=()=>props.window.kind==="rate_limit"?"API \xB7 ":props.window.kind==="configured_limit"?"Limit \xB7 ":""
355
+ const measurement=()=>{if(props.window.used!==undefined)
356
+ return `${props.window.used}${props.window.total === undefined ? "" : `/${props.window.total}`}${props.window.unit === "%" ? "" : " "}${props.window.unit} used`
357
+ if(props.window.remaining!==undefined)
358
+ return `${props.window.remaining} ${props.window.unit} remaining`
359
+ return `${props.window.total} ${props.window.unit} capacity \xB7 usage unknown`
360
+ }
361
+ return(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("text"),_el$3=_$createElement("text"),_el$4=_$createElement("text")
325
362
  _$insertNode(_el$,_el$2)
326
- _$insertNode(_el$,_el$6)
327
- _$insertNode(_el$2,_el$3)
328
- _$insertNode(_el$2,_el$5)
363
+ _$insertNode(_el$,_el$3)
364
+ _$insertNode(_el$,_el$4)
329
365
  _$setProp(_el$2,"wrapMode","none")
330
366
  _$setProp(_el$2,"truncate",true)
367
+ _$insert(_el$2,kind,null)
368
+ _$insert(_el$2,()=>props.window.label,null)
331
369
  _$insert(_el$2,(()=>{var _c$=_$memo(()=>props.window.durationMinutes===undefined)
332
- return()=>_c$()?props.window.label:`${props.window.durationMinutes} min`
333
- })(),_el$3)
334
- _$insert(_el$2,()=>props.window.used,_el$5)
335
- _$insert(_el$2,(()=>{var _c$2=_$memo(()=>props.window.total===undefined)
336
- return()=>_c$2()?"":`/${props.window.total}`
337
- })(),_el$5)
338
- _$insert(_el$2,()=>props.window.unit,_el$5)
339
- _$setProp(_el$6,"wrapMode","word")
340
- _$insert(_el$6,()=>reset().replace(" \xB7 ","")||"Reset time unavailable")
341
- _$effect((_p$)=>{var _v$=props.api.theme.current.text,_v$2=props.api.theme.current.textMuted
370
+ return()=>_c$()?"":` \xB7 ${props.window.durationMinutes} min`
371
+ })(),null)
372
+ _$setProp(_el$3,"wrapMode","word")
373
+ _$insert(_el$3,measurement)
374
+ _$setProp(_el$4,"wrapMode","word")
375
+ _$insert(_el$4,(()=>{var _c$2=_$memo(()=>props.window.resetsAt===undefined)
376
+ return()=>_c$2()?"Reset time unavailable":`resets ${new Date(props.window.resetsAt * 1000).toLocaleString()}`
377
+ })())
378
+ _$effect((_p$)=>{var _v$=props.api.theme.current.text,_v$2=props.api.theme.current.text,_v$3=props.api.theme.current.textMuted
342
379
  _v$!==_p$.e&&(_p$.e=_$setProp(_el$2,"fg",_v$,_p$.e))
343
- _v$2!==_p$.t&&(_p$.t=_$setProp(_el$6,"fg",_v$2,_p$.t))
380
+ _v$2!==_p$.t&&(_p$.t=_$setProp(_el$3,"fg",_v$2,_p$.t))
381
+ _v$3!==_p$.a&&(_p$.a=_$setProp(_el$4,"fg",_v$3,_p$.a))
344
382
  return _p$
345
- },{e:undefined,t:undefined
383
+ },{e:undefined,t:undefined,a:undefined
346
384
  })
347
385
  return _el$
348
386
  })()
@@ -362,9 +400,9 @@ return
362
400
  setBusy(true)
363
401
  const previous=input.preferences.pendingResetAttempt()
364
402
  const attempt=previous??{providerID:input.model.providerID,modelID:input.model.modelID,accountID:input.model.accountID,...creditID&&{creditID
365
- },idempotencyKey:randomUUID(),createdAt:Date.now()
366
- }
367
- try{if(attempt.accountID!==input.model.accountID||attempt.modelID!==input.model.modelID||attempt.providerID!==input.model.providerID){throw new Error("Resolve the earlier attempt on its linked account and model first")
403
+ },idempotencyKey:randomUUID(),createdAt:Date.now(),...input.model.hostConnection?.id&&{connectionID:input.model.hostConnection.id
404
+ }}
405
+ try{if(attempt.accountID!==input.model.accountID||attempt.modelID!==input.model.modelID||attempt.providerID!==input.model.providerID||attempt.connectionID!==undefined&&attempt.connectionID!==input.model.hostConnection?.id){throw new Error("Resolve the earlier attempt on its linked account and model first")
368
406
  }
369
407
  if(!previous)
370
408
  await input.preferences.beginResetAttempt(attempt)
@@ -427,23 +465,156 @@ else if(option.value.startsWith("credit:"))
427
465
  actions.review(option.value.slice("credit:".length)||undefined)
428
466
  }})
429
467
  }
430
- import {insert as _$insert5} from "@opentui/solid"
431
- import {setProp as _$setProp5} from "@opentui/solid"
432
- import {effect as _$effect5} from "@opentui/solid"
433
- import {createTextNode as _$createTextNode3} from "@opentui/solid"
434
- import {insertNode as _$insertNode3} from "@opentui/solid"
435
- import {createElement as _$createElement5} from "@opentui/solid"
436
- import {memo as _$memo3} from "@opentui/solid"
437
- import {createComponent as _$createComponent7} from "@opentui/solid"
438
- import {For,Show as Show3} from "solid-js"
439
- import {useDialogs as useDialogs2,useIcons as useIcons2} from "./tui.js"
440
468
  import {createComponent as _$createComponent4} from "@opentui/solid"
469
+ import {effect as _$effect3} from "@opentui/solid"
470
+ import {insert as _$insert3} from "@opentui/solid"
471
+ import {memo as _$memo3} from "@opentui/solid"
472
+ import {setProp as _$setProp3} from "@opentui/solid"
473
+ import {createElement as _$createElement3} from "@opentui/solid"
474
+ import {For} from "solid-js"
441
475
  import {effect as _$effect2} from "@opentui/solid"
476
+ import {memo as _$memo2} from "@opentui/solid"
477
+ import {createTextNode as _$createTextNode} from "@opentui/solid"
478
+ import {insertNode as _$insertNode2} from "@opentui/solid"
442
479
  import {insert as _$insert2} from "@opentui/solid"
443
480
  import {setProp as _$setProp2} from "@opentui/solid"
444
481
  import {createElement as _$createElement2} from "@opentui/solid"
482
+ function QuotaBalanceRow(props){return(()=>{var _el$=_$createElement2("box"),_el$2=_$createElement2("text"),_el$3=_$createTextNode(` \xB7 `),_el$4=_$createElement2("text")
483
+ _$insertNode2(_el$,_el$2)
484
+ _$insertNode2(_el$,_el$4)
485
+ _$insertNode2(_el$2,_el$3)
486
+ _$setProp2(_el$2,"wrapMode","word")
487
+ _$insert2(_el$2,()=>props.balance.label,_el$3)
488
+ _$insert2(_el$2,()=>props.balance.scope,null)
489
+ _$setProp2(_el$4,"wrapMode","word")
490
+ _$insert2(_el$4,(()=>{var _c$=_$memo2(()=>!!props.balance.unlimited)
491
+ return()=>_c$()?"No configured cap":`${props.balance.amount} ${props.balance.unit ?? "(provider unit not specified)"}`
492
+ })())
493
+ _$effect2((_p$)=>{var _v$=props.api.theme.current.textMuted,_v$2=props.api.theme.current.text
494
+ _v$!==_p$.e&&(_p$.e=_$setProp2(_el$2,"fg",_v$,_p$.e))
495
+ _v$2!==_p$.t&&(_p$.t=_$setProp2(_el$4,"fg",_v$2,_p$.t))
496
+ return _p$
497
+ },{e:undefined,t:undefined
498
+ })
499
+ return _el$
500
+ })()
501
+ }
502
+ function QuotaSnapshotRows(props){return [(()=>{var _el$=_$createElement3("text")
503
+ _$setProp3(_el$,"wrapMode","none")
504
+ _$setProp3(_el$,"truncate",true)
505
+ _$insert3(_el$,()=>props.snapshot.providerName,null)
506
+ _$insert3(_el$,(()=>{var _c$=_$memo3(()=>!!props.snapshot.accountId)
507
+ return()=>_c$()?` \xB7 ${props.snapshot.accountId}`:""
508
+ })(),null)
509
+ _$effect3((_$p)=>_$setProp3(_el$,"fg",props.api.theme.current.textMuted,_$p))
510
+ return _el$
511
+ })(),_$createComponent4(For,{get each(){return props.snapshot.windows
512
+ },children:(window)=>_$createComponent4(QuotaWindowRow,{get api(){return props.api
513
+ },window
514
+ })
515
+ }),_$createComponent4(For,{get each(){return props.snapshot.balances
516
+ },children:(balance)=>_$createComponent4(QuotaBalanceRow,{get api(){return props.api
517
+ },balance
518
+ })
519
+ })]
520
+ }
521
+ import {createComponent as _$createComponent5} from "@opentui/solid"
522
+ import {createSignal as createSignal4,onMount} from "solid-js"
523
+ import {SelectionMenu as SelectionMenu2,useDialogs as useDialogs2} from "./tui.js"
524
+ var rows=[["openai","OpenAI / Codex",["subscription","api-rate-limits"],"Codex account RPC; HTTP response request/token limits for API connections.","https://developers.openai.com/api/docs/guides/rate-limits"],["anthropic","Anthropic / Claude",["api-rate-limits","configured-limits"],"Model HTTP headers; eligible organization/admin credentials can read configured model limits. Claude subscription windows are separate.","https://platform.claude.com/docs/en/manage-claude/rate-limits-api"],["openrouter","OpenRouter",["key-budget","account-balance"],"Inference-key budget; account credits require a management key. Free-model request limits apply only to free variants.","https://openrouter.ai/docs/api/api-reference/api-keys/get-current-api-key"],["deepseek","DeepSeek",["account-balance"],"Provider-reported USD/CNY available, granted and topped-up balances.","https://api-docs.deepseek.com/api/get-user-balance"],["moonshotai","Moonshot / Kimi API",["account-balance"],"International USD balances; coding subscriptions are a different product.","https://platform.moonshot.ai/docs/api/balance"],["moonshotai-cn","Moonshot / Kimi API China",["account-balance"],"China CNY balances; never reuse an international key against the China endpoint.","https://platform.moonshot.cn/docs/api/balance"],["minimax","MiniMax",["subscription","account-balance"],"Token Plan model rows/explicit percentages; official SDK balance response for pay-as-you-go keys.","https://platform.minimax.io/docs/token-plan/faq"],["minimax-cn","MiniMax China",["subscription","account-balance"],"Region-specific native Token Plan and SDK balance endpoints.","https://github.com/MiniMax-AI/cli/tree/main/src/sdk/quota"],["github-copilot","GitHub Copilot",["subscription"],"Official SDK token-scoped quota/model RPC; no direct copilot_internal calls or ambient CLI login.","https://github.com/github/copilot-sdk/blob/main/docs/features/usage-and-billing.md"],["groq","Groq",["api-rate-limits"],"Native model response headers: requests per day and tokens per minute.","https://console.groq.com/docs/rate-limits"],["fireworks-ai","Fireworks",["configured-limits","api-rate-limits"],"Native model token/second capacities and account monthly spend quota; GPU quotas are excluded.","https://docs.fireworks.ai/serverless/rate-limits"],["siliconflow","SiliconFlow",["account-balance"],"Native current-user balances; retain unknown currency when the API does not report it.","https://docs.siliconflow.com/en/api-reference/userinfo/get-user-info"],["siliconflow-cn","SiliconFlow China",["account-balance"],"Region-specific current-user balances; currency is unknown when the API omits it.","https://docs.siliconflow.cn/"],["novita-ai","Novita",["account-balance"],"Native current-user balances in 1/10000 USD.","https://novita.ai/docs/api-reference/basic-get-user-balance"],["poe","Poe",["account-balance"],"API account point balance; includes plan and add-on points, not session token usage.","https://creator.poe.com/docs/resources/usage-api"],["nano-gpt","NanoGPT",["account-balance","subscription"],"Native USD/Nano balance and documented subscription token windows; null/degraded counters remain unknown.","https://docs.nano-gpt.com/api-reference/endpoint/subscription-usage"],["aihubmix","AIHubMix",["account-balance"],"Current-account balance API needs a Manage Key; inference keys do not grant management access.","https://docs.aihubmix.com/en/api/CliEndpoints/get-self"],["azure","Azure OpenAI",["api-rate-limits"],"Remaining request/token headers for the actual deployment; no ChatGPT subscription or inferred cloud balance.","https://learn.microsoft.com/en-us/azure/ai-foundry/openai/quotas-limits"],["cerebras","Cerebras",["api-rate-limits"],"Only self-describing native counters actually returned for the selected model; console tier examples are not live quota.","https://inference-docs.cerebras.ai/support/rate-limits"],["mistral","Mistral",[],"Documented remaining header lacks a verified unit/window association; no inferred subscription balance.","https://docs.mistral.ai/resources/known-limitations"],["togetherai","Together AI",[],"Dynamic model limits expose retry intervals, not remaining quota; billing UI does not establish a remaining-credit API.","https://docs.together.ai/docs/rate-limits"],["xai","xAI / Grok",[],"Management billing needs separate team credentials; no verified portable remaining API for this inference connection.","https://docs.x.ai/"],["perplexity","Perplexity",[],"No verified remaining-credit API for an inference key. API credits, consumer subscriptions and Computer credits are distinct.","https://www.perplexity.ai/help-center/en/articles/10354847-api-payment-and-billing"],["google","Google Gemini",["configured-limits"],"Native model-matched QuotaFailure responses when provided; cloud quota permissions differ from an inference key.","https://ai.google.dev/gemini-api/docs/rate-limits"],["google-vertex","Google Vertex AI",["configured-limits"],"Native model-matched quota error details; cloud project/region management and dynamic shared quotas are separate.","https://cloud.google.com/vertex-ai/generative-ai/docs/quotas"],["amazon-bedrock","Amazon Bedrock",[],"Service Quotas/CloudWatch require cloud IAM/region/model mapping not exposed by a portable inference connection.","https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html"],["cloudflare-workers-ai","Cloudflare Workers AI",[],"Account analytics do not prove remaining free allowance; separate account management capability is required.","https://developers.cloudflare.com/workers-ai/platform/limits/"],["huggingface","Hugging Face Inference Providers",[],"Hub request quota is not an inference-model quota or inference-credit balance.","https://huggingface.co/docs/inference-providers/pricing"],["zai","Z.ai / GLM Coding Plan",[],"No verified external quota contract for monitor/dashboard APIs; private endpoints are excluded.","https://docs.z.ai/devpack/overview"],["kimi-for-coding","Kimi Code subscription",[],"Keep separate from Moonshot API balance; an external subscription usage contract/model mapping must be verified.","https://www.kimi.com/code/docs/en/"],["alibaba","Alibaba Cloud / Qwen / DashScope",[],"Cloud account/project billing permissions and model mapping are required; browser OAuth state is not a quota source.","https://www.alibabacloud.com/help/en/model-studio/"],["opencode","OpenCode Zen / Go",[],"Session token/cost statistics are not provider quota; no undocumented billing endpoints.","https://opencode.ai/docs/zen/"],["cursor","Cursor",[],"Consumer usage needs a supported external API; no cookie or credential-file integrations.","https://docs.cursor.com/"],["windsurf","Windsurf / Codeium",[],"Consumer plan windows need a supported external API; no private account endpoints.","https://docs.windsurf.com/"],["amp","Amp",[],"Product credits need a supported external account API; no cookie or local credential extraction.","https://ampcode.com/manual"],["ollama","Ollama",[],"Local inference has no provider subscription window; cloud products require their own account API.","https://docs.ollama.com/api"],["lmstudio","LM Studio",[],"Local inference has no provider subscription quota to read.","https://lmstudio.ai/docs/developer"],["nvidia","NVIDIA NIM / NGC",[],"Inference access does not identify cloud/trial NGC credit balance or self-hosted resource quota.","https://docs.nvidia.com/nim/"],["cohere","Cohere",[],"No verified native remaining-counter contract for this integration; static organization tier examples are not live quotas.","https://docs.cohere.com/docs/rate-limits"],["ai21","AI21",[],"No verified native remaining-counter contract or subscription balance endpoint for this integration.","https://docs.ai21.com/"],["replicate","Replicate",[],"Prediction cost is not remaining account credit; no inferred balance from execution cost.","https://replicate.com/docs/topics/billing"],["chutes","Chutes",[],"Current-user quota APIs exist, but their public schemas do not establish native units/model allowance semantics yet.","https://chutes.ai/docs/api-reference/users"],["requesty","Requesty",[],"Enterprise management key and key ID are required; dated usage reports alone are not remaining budget.","https://docs.requesty.ai/features/key-management-api"],["baseten","Baseten",[],"No independently verified portable remaining quota API for the configured inference connection.","https://docs.baseten.co/"],["modelscope","ModelScope",[],"No independently verified portable remaining quota API/model mapping for the selected connection.","https://opencode.ai/docs/providers/"],["nebius","Nebius",[],"Cloud billing/management access is separate from a plain inference credential.","https://docs.nebius.com/"],["vultr","Vultr",[],"Cloud management quota/billing permissions are separate from inference access.","https://docs.vultr.com/"],["v0","Vercel v0",[],"Consumer/product credits need their own documented external account API.","https://v0.dev/docs"]]
525
+ var PROVIDER_QUOTA_CAPABILITIES= rows.map(([id,name,sources,reason,url])=>({id,name,sources,reason,documentation:[url]}))
526
+ var aliases={fireworks:"fireworks-ai",novita:"novita-ai",moonshot:"moonshotai","minimax-coding-plan":"minimax","minimax-coding-plan-cn":"minimax-cn",together:"togetherai","silicon-flow":"siliconflow","github-copilot-enterprise":"github-copilot","google-vertex-anthropic":"google-vertex","z-ai":"zai",zhipuai:"zai",dashscope:"alibaba","alibaba-cn":"alibaba",qwen:"alibaba","opencode-go":"opencode","opencode-zen":"opencode",codeium:"windsurf","azure-cognitive-services":"azure"
527
+ }
528
+ function providerQuotaCapability(id,canonical){const key=aliases[canonical??id]??canonical??id
529
+ const found=PROVIDER_QUOTA_CAPABILITIES.find((entry)=>entry.id===key)
530
+ return found??{id,name:id,sources:[],reason:"No independent quota/balance API or native counter semantics are verified for this provider.",documentation:["https://opencode.ai/docs/providers/"]
531
+ }}
532
+ function providerQuotaCatalog(providers=[]){const entries=new Map(PROVIDER_QUOTA_CAPABILITIES.map((entry)=>[entry.id,entry]))
533
+ for(const provider of providers)
534
+ entries.set(provider.id,{...providerQuotaCapability(provider.id,provider.canonical),id:provider.id,name:provider.name
535
+ })
536
+ return [...entries.values()].sort((a,b)=>a.name.localeCompare(b.name))
537
+ }
538
+ function ProviderSupportDialog(props){const dialogs=useDialogs2(props.api)
539
+ const [entries,setEntries]=createSignal4(providerQuotaCatalog())
540
+ onMount(()=>{if(props.load)
541
+ props.load().then(setEntries).catch(()=>{})
542
+ })
543
+ return _$createComponent5(SelectionMenu2,{get api(){return props.api
544
+ },get title(){return `Provider sources \xB7 ${entries().length}`
545
+ },height:18,get options(){return [...entries().map((entry)=>({title:entry.name,value:entry.id,icon:entry.sources.length>0?"info":"help",description:`${entry.sources.length > 0 ? entry.sources.join(", ") : "API-dependent"} \xB7 ${entry.reason}`
546
+ })),{title:"Close",value:"close",description:"Return to the sidebar",icon:"close"
547
+ }]
548
+ },onSelect:(option)=>{if(option.value==="close")
549
+ dialogs.back()
550
+ }})
551
+ }
552
+ var text={type:"string",minLength:1,maxLength:512}
553
+ var model2={type:"object",properties:{providerID:text,modelID:text,sessionID:text,connectionID:text,variant:text,force:{type:"boolean"}},required:["providerID","modelID","sessionID"],additionalProperties:false
554
+ }
555
+ var NavigatorQuotaRpc={id:"opencode-navigator.quotas",methods:{read:{input:model2,output:{type:"object",additionalProperties:true}},capabilities:{input:{type:"object",additionalProperties:false},output:{type:"array",items:{type:"object",additionalProperties:true}}}},events:{updated:{schema:{type:"object",properties:{providerID:text,modelID:text},required:["providerID","modelID"],additionalProperties:false
556
+ }}}};
557
+ function missing(error){return typeof error==="object"&&error!==null&&"type" in error&&error.type==="rpc.unavailable"
558
+ }
559
+ function createNativeQuotaAdapter(context){const rpc=context.client.rpc(NavigatorQuotaRpc)
560
+ const listeners=new Set
561
+ const unsubscribe=rpc.events.on("updated",(event)=>{const location=context.location??context.data.location.default()
562
+ if(event.location.directory!==location.directory)
563
+ return
564
+ if(typeof event.data.providerID!=="string"||typeof event.data.modelID!=="string")
565
+ return
566
+ for(const listener of listeners)
567
+ listener(event.data.providerID,event.data.modelID)
568
+ })
569
+ const adapter={id:"provider-api",name:"Provider-native quotas",supports:(model)=>model.hostConnection?.status!=="loading",async read(model,signal,force){const base={model,providerId:model.providerID,providerName:model.providerID,availability:"setup_required",fetchedAt:Date.now(),windows:[]
570
+ }
571
+ try{if(typeof rpc.read!=="function")
572
+ return{...base,message:"Enable the Navigator OpenCode 2 server plugin for native provider quotas and balances."
573
+ }
574
+ const value=await rpc.read({providerID:model.providerID,modelID:model.modelID,sessionID:model.sessionID,...model.variant&&{variant:model.variant},...model.hostConnection?.id&&{connectionID:model.hostConnection.id},force
575
+ },{location:context.location??context.data.location.default(),signal})
576
+ if(model.hostConnection?.id&&value.connectionID&&value.connectionID!==model.hostConnection.id)
577
+ return{...base,availability:"unauthenticated",message:"The provider connection changed during this read."
578
+ }
579
+ return normalizeProviderQuota({model,providerId:value.providerId,providerName:value.providerName,availability:value.availability,fetchedAt:value.fetchedAt,windows:value.windows,...value.balances&&{balances:value.balances},...value.accountId&&{accountId:value.accountId},...value.connectionID&&{connectionID:value.connectionID},...value.message&&{message:value.message}})
580
+ } catch(error){if(!missing(error))
581
+ throw error
582
+ return{...base,message:"Enable the Navigator OpenCode 2 server plugin for native provider quotas and balances."
583
+ }}},dispose(){unsubscribe()
584
+ listeners.clear()
585
+ }}
586
+ return{adapter,onUpdated(listener){listeners.add(listener)
587
+ return()=>listeners.delete(listener)
588
+ },async capabilities(){try{return await rpc.capabilities({})
589
+ } catch{return providerQuotaCatalog(context.data.location.provider?.list?.(context.location)??[])
590
+ }}}}
591
+ import {mergeProps as _$mergeProps} from "@opentui/solid"
592
+ import {insert as _$insert7} from "@opentui/solid"
593
+ import {setProp as _$setProp8} from "@opentui/solid"
594
+ import {effect as _$effect8} from "@opentui/solid"
595
+ import {createTextNode as _$createTextNode4} from "@opentui/solid"
596
+ import {insertNode as _$insertNode5} from "@opentui/solid"
597
+ import {createElement as _$createElement8} from "@opentui/solid"
598
+ import {memo as _$memo6} from "@opentui/solid"
599
+ import {createComponent as _$createComponent10} from "@opentui/solid"
600
+ import {Show as Show4} from "solid-js"
601
+ import {useDialogs as useDialogs4,useIcons as useIcons3} from "./tui.js"
602
+ import {setProp as _$setProp5} from "@opentui/solid"
603
+ import {effect as _$effect5} from "@opentui/solid"
604
+ import {createTextNode as _$createTextNode2} from "@opentui/solid"
605
+ import {insertNode as _$insertNode3} from "@opentui/solid"
606
+ import {createElement as _$createElement5} from "@opentui/solid"
607
+ import {memo as _$memo4} from "@opentui/solid"
608
+ import {createComponent as _$createComponent7} from "@opentui/solid"
609
+ import {Show} from "solid-js"
610
+ import {useDialogs as useDialogs3,useIcons} from "./tui.js"
611
+ import {createComponent as _$createComponent6} from "@opentui/solid"
612
+ import {effect as _$effect4} from "@opentui/solid"
613
+ import {insert as _$insert4} from "@opentui/solid"
614
+ import {setProp as _$setProp4} from "@opentui/solid"
615
+ import {createElement as _$createElement4} from "@opentui/solid"
445
616
  import {SelectionBox} from "./tui.js"
446
- import {createSignal as createSignal4,onCleanup} from "solid-js"
617
+ import {createSignal as createSignal5,onCleanup} from "solid-js"
447
618
  function sidebarInteractiveColors(theme,state,normalColor=theme.text,mode="row"){const highlighted=!state.disabled&&(state.focused||mode==="control"&&state.hovered)
448
619
  let backgroundColor=theme.backgroundPanel
449
620
  let foregroundColor=normalColor
@@ -459,7 +630,7 @@ else if(highlighted)
459
630
  foregroundColor=theme.selectedListItemText
460
631
  return{backgroundColor,foregroundColor}}
461
632
  function useSidebarItem(api,interaction,descriptor,normalColor=()=>api.theme.current.text,mode="row"){let unregister
462
- const [hovered,setHovered]=createSignal4(false)
633
+ const [hovered,setHovered]=createSignal5(false)
463
634
  const disabled=()=>descriptor.disabled?.()===true
464
635
  const focused=()=>Boolean(interaction?.ownsFocus()&&interaction.isSelected(descriptor.id))
465
636
  const colors=()=>sidebarInteractiveColors(api.theme.current,{focused:focused(),hovered:hovered(),disabled:disabled()},normalColor(),mode)
@@ -477,35 +648,69 @@ return true
477
648
  }}}
478
649
  function LimitsAction(props){const item=useSidebarItem(props.api,props.interaction,{id:props.id,position:()=>props.position,disabled:()=>props.disabled===true,activate:props.onActivate
479
650
  })
480
- return _$createComponent4(SelectionBox,{ref:(node)=>item.ref(node),get id(){return props.id
651
+ return _$createComponent6(SelectionBox,{ref:(node)=>item.ref(node),get id(){return props.id
481
652
  },height:1,get backgroundColor(){return item.backgroundColor()
482
653
  },get onMouseOver(){return item.onMouseOver
483
654
  },get onMouseOut(){return item.onMouseOut
484
655
  },onMouseUp:(event)=>{event.stopPropagation()
485
656
  item.activate(event)
486
- },get children(){var _el$=_$createElement2("text")
487
- _$setProp2(_el$,"wrapMode","none")
488
- _$setProp2(_el$,"truncate",true)
489
- _$insert2(_el$,()=>props.label)
490
- _$effect2((_$p)=>_$setProp2(_el$,"fg",item.foregroundColor(),_$p))
657
+ },get children(){var _el$=_$createElement4("text")
658
+ _$setProp4(_el$,"wrapMode","none")
659
+ _$setProp4(_el$,"truncate",true)
660
+ _$insert4(_el$,()=>props.label)
661
+ _$effect4((_$p)=>_$setProp4(_el$,"fg",item.foregroundColor(),_$p))
491
662
  return _el$
492
663
  }})
493
664
  }
494
- import {createComponent as _$createComponent5} from "@opentui/solid"
495
- import {effect as _$effect3} from "@opentui/solid"
496
- import {insert as _$insert3} from "@opentui/solid"
497
- import {setProp as _$setProp3} from "@opentui/solid"
498
- import {createElement as _$createElement3} from "@opentui/solid"
499
- import {Show} from "solid-js"
500
- function LimitsStatus(props){const message=()=>{const snapshot=props.controller.snapshot()
665
+ function CodexAccountControls(props){const icons=useIcons()
666
+ const model=()=>props.controller.current().model
667
+ const accountLink=createCodexAccountLink({api:props.api,preferences:props.preferences,adapter:props.codex,model,dialogs:useDialogs3(props.api)
668
+ })
669
+ const eligible=()=>(props.model.providerID==="openai"||props.model.hostConnection?.providerID==="openai")&&props.model.hostConnection?.method!=="api"
670
+ const action=(id,row,label,onActivate)=>_$createComponent7(LimitsAction,{get api(){return props.api
671
+ },get interaction(){return props.interaction
672
+ },id:`${PLUGIN_ID}.limits.${id}`,get position(){return{section:props.navigationSection,row,column:0
673
+ }},label,onActivate,get disabled(){return accountLink.busy()
674
+ }})
675
+ return _$createComponent7(Show,{get when(){return eligible()
676
+ },get children(){return [_$createComponent7(Show,{get when(){return _$memo4(()=>!!!props.model.accountID)()&&(!props.model.hostConnection||props.model.hostConnection.status==="ready")
677
+ },get children(){return [(()=>{var _el$=_$createElement5("text")
678
+ _$insertNode3(_el$,_$createTextNode2(`Link the matching Codex account once for this connection`))
679
+ _$effect5((_$p)=>_$setProp5(_el$,"fg",props.api.theme.current.textMuted,_$p))
680
+ return _el$
681
+ })(),_$memo4(()=>action("link",1,`${icons.icon("scope")} Link Codex CLI account`,()=>void accountLink.link()))]
682
+ }}),_$createComponent7(Show,{get when(){return _$memo4(()=>!!props.model.accountID)()&&props.model.accountSource!=="host"
683
+ },get children(){return [_$memo4(()=>action("unlink",4,`${icons.icon("close")} Unlink Codex account`,()=>props.preferences.setCodexConnectionBinding(props.model.providerID,props.model.hostConnection?.id))),_$createComponent7(Show,{get when(){return props.controller.snapshot()?.availability==="unauthenticated"
684
+ },get children(){return action("relink",5,`${icons.icon("scope")} Relink Codex account`,()=>void accountLink.link())
685
+ }})]
686
+ }})]
687
+ }})
688
+ }
689
+ import {createComponent as _$createComponent8} from "@opentui/solid"
690
+ import {effect as _$effect6} from "@opentui/solid"
691
+ import {insert as _$insert5} from "@opentui/solid"
692
+ import {setProp as _$setProp6} from "@opentui/solid"
693
+ import {createElement as _$createElement6} from "@opentui/solid"
694
+ import {Show as Show2} from "solid-js"
695
+ function LimitsStatus(props){const message=()=>{const model=props.controller.current().model
696
+ const connection=model?.hostConnection
697
+ const snapshot=props.controller.snapshot()
501
698
  const state=props.controller.state()
502
699
  if(state.status==="error")
503
700
  return snapshot?"Stale \xB7 refresh failed; retry below":"Quota read failed \xB7 retry below"
701
+ if(snapshot?.message)
702
+ return snapshot.availability==="stale"?`Stale \xB7 ${snapshot.message}`:snapshot.availability==="ready"?`Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()} \xB7 ${snapshot.message}`:snapshot.message
703
+ if(connection?.status==="loading")
704
+ return "Checking the OpenCode account\u2026"
705
+ if(connection?.status==="unavailable")
706
+ return "OpenCode account metadata unavailable \xB7 refresh below"
707
+ if(connection?.status==="unsupported")
708
+ return "Codex quotas require an active ChatGPT OAuth connection"
504
709
  if(!snapshot)
505
- return state.status==="loading"?"Reading provider limits\u2026":"Refresh to read provider limits"
710
+ return state.status==="loading"?"Reading provider limits\u2026":props.controller.current().adapter?"Refresh to read provider limits":providerQuotaCapability(model?.providerID??"").reason
506
711
  const availability=snapshot.availability
507
712
  if(availability==="unauthenticated")
508
- return "Codex sign-in or linked account changed \xB7 relink below"
713
+ return model?.accountSource==="host"?"Sign in to Codex CLI with the account selected in OpenCode":"Codex sign-in or linked account changed \xB7 relink below"
509
714
  if(availability==="unsupported")
510
715
  return "No documented quota bucket for this model"
511
716
  if(availability==="rate_limited")
@@ -514,24 +719,24 @@ if(availability==="stale")
514
719
  return "Stale provider data \xB7 refresh below"
515
720
  return `Updated ${new Date(snapshot.fetchedAt).toLocaleTimeString()}`
516
721
  }
517
- return _$createComponent5(Show,{get when(){return props.controller.current().adapter
518
- },get children(){var _el$=_$createElement3("text")
519
- _$setProp3(_el$,"wrapMode","word")
520
- _$insert3(_el$,message)
521
- _$effect3((_$p)=>_$setProp3(_el$,"fg",props.api.theme.current.textMuted,_$p))
722
+ return _$createComponent8(Show2,{get when(){return props.controller.current().model
723
+ },get children(){var _el$=_$createElement6("text")
724
+ _$setProp6(_el$,"wrapMode","word")
725
+ _$insert5(_el$,message)
726
+ _$effect6((_$p)=>_$setProp6(_el$,"fg",props.api.theme.current.textMuted,_$p))
522
727
  return _el$
523
728
  }})
524
729
  }
525
- import {createComponent as _$createComponent6} from "@opentui/solid"
526
- import {effect as _$effect4} from "@opentui/solid"
527
- import {createTextNode as _$createTextNode2} from "@opentui/solid"
528
- import {insertNode as _$insertNode2} from "@opentui/solid"
529
- import {memo as _$memo2} from "@opentui/solid"
530
- import {insert as _$insert4} from "@opentui/solid"
531
- import {setProp as _$setProp4} from "@opentui/solid"
532
- import {createElement as _$createElement4} from "@opentui/solid"
533
- import {Show as Show2,createEffect as createEffect2,createSignal as createSignal5} from "solid-js"
534
- import {SelectionBox as SelectionBox2,useIcons} from "./tui.js"
730
+ import {createComponent as _$createComponent9} from "@opentui/solid"
731
+ import {effect as _$effect7} from "@opentui/solid"
732
+ import {createTextNode as _$createTextNode3} from "@opentui/solid"
733
+ import {insertNode as _$insertNode4} from "@opentui/solid"
734
+ import {memo as _$memo5} from "@opentui/solid"
735
+ import {insert as _$insert6} from "@opentui/solid"
736
+ import {setProp as _$setProp7} from "@opentui/solid"
737
+ import {createElement as _$createElement7} from "@opentui/solid"
738
+ import {Show as Show3,createEffect as createEffect2,createSignal as createSignal6} from "solid-js"
739
+ import {SelectionBox as SelectionBox2,useIcons as useIcons2} from "./tui.js"
535
740
  var graphemes=new Intl.Segmenter(undefined,{granularity:"grapheme"
536
741
  })
537
742
  function truncateEnd(value,maxWidth){if(Bun.stringWidth(value)<=maxWidth)
@@ -548,8 +753,8 @@ result += segment
548
753
  }
549
754
  return result + ellipsis
550
755
  }
551
- function Section(props){const icons=useIcons()
552
- const [actionWidth,setActionWidth]=createSignal5()
756
+ function Section(props){const icons=useIcons2()
757
+ const [actionWidth,setActionWidth]=createSignal6()
553
758
  const icon=()=>props.section?icons.section(props.section):props.icon
554
759
  const theme=()=>props.api.theme.current
555
760
  const item=useSidebarItem(props.api,props.interaction,{id:props.sectionId??`section.${props.title}`,position:()=>({section:props.navigationSection??0,row:0,column:0
@@ -565,163 +770,140 @@ const actionLabel=()=>{const label=props.headerAction?.label()??""
565
770
  const width=actionWidth()
566
771
  return width===undefined?label:truncateEnd(label,width-2)
567
772
  }
568
- return(()=>{var _el$=_$createElement4("box")
569
- _$setProp4(_el$,"gap",1)
570
- _$insert4(_el$,_$createComponent6(SelectionBox2,{ref:(node)=>item.ref(node),get id(){return props.sectionId
773
+ return(()=>{var _el$=_$createElement7("box")
774
+ _$setProp7(_el$,"gap",1)
775
+ _$insert6(_el$,_$createComponent9(SelectionBox2,{ref:(node)=>item.ref(node),get id(){return props.sectionId
571
776
  },flexDirection:"row",height:1,justifyContent:"space-between",gap:1,paddingLeft:1,paddingRight:1,get backgroundColor(){return item.backgroundColor()
572
777
  },get onMouseOver(){return item.onMouseOver
573
778
  },get onMouseOut(){return item.onMouseOut
574
- },onMouseDown:(event)=>item.activate(event),onSizeChange:()=>setActionWidth(undefined),get children(){return [(()=>{var _el$2=_$createElement4("text"),_el$3=_$createElement4("span"),_el$4=_$createTextNode2(` `),_el$5=_$createElement4("span"),_el$6=_$createElement4("b")
575
- _$insertNode2(_el$2,_el$3)
576
- _$insertNode2(_el$2,_el$4)
577
- _$insertNode2(_el$2,_el$5)
578
- _$insertNode2(_el$2,_el$6)
579
- _$setProp4(_el$2,"flexShrink",0)
580
- _$setProp4(_el$2,"wrapMode","none")
581
- _$insert4(_el$3,()=>icons.icon(props.open?"expanded":"collapsed"))
582
- _$insert4(_el$5,(()=>{var _c$=_$memo2(()=>!!icon())
779
+ },onMouseDown:(event)=>item.activate(event),onSizeChange:()=>setActionWidth(undefined),get children(){return [(()=>{var _el$2=_$createElement7("text"),_el$3=_$createElement7("span"),_el$4=_$createTextNode3(` `),_el$5=_$createElement7("span"),_el$6=_$createElement7("b")
780
+ _$insertNode4(_el$2,_el$3)
781
+ _$insertNode4(_el$2,_el$4)
782
+ _$insertNode4(_el$2,_el$5)
783
+ _$insertNode4(_el$2,_el$6)
784
+ _$setProp7(_el$2,"flexShrink",0)
785
+ _$setProp7(_el$2,"wrapMode","none")
786
+ _$insert6(_el$3,()=>icons.icon(props.open?"expanded":"collapsed"))
787
+ _$insert6(_el$5,(()=>{var _c$=_$memo5(()=>!!icon())
583
788
  return()=>_c$()?`${icon()} `:""
584
789
  })())
585
- _$insert4(_el$6,()=>props.title)
586
- _$effect4((_p$)=>{var _v$=item.foregroundColor(),_v$2={fg:item.focused()?item.foregroundColor():theme().accent
790
+ _$insert6(_el$6,()=>props.title)
791
+ _$effect7((_p$)=>{var _v$=item.foregroundColor(),_v$2={fg:item.focused()?item.foregroundColor():theme().accent
587
792
  },_v$3={fg:item.focused()?item.foregroundColor():theme().accent
588
793
  }
589
- _v$!==_p$.e&&(_p$.e=_$setProp4(_el$2,"fg",_v$,_p$.e))
590
- _v$2!==_p$.t&&(_p$.t=_$setProp4(_el$3,"style",_v$2,_p$.t))
591
- _v$3!==_p$.a&&(_p$.a=_$setProp4(_el$5,"style",_v$3,_p$.a))
794
+ _v$!==_p$.e&&(_p$.e=_$setProp7(_el$2,"fg",_v$,_p$.e))
795
+ _v$2!==_p$.t&&(_p$.t=_$setProp7(_el$3,"style",_v$2,_p$.t))
796
+ _v$3!==_p$.a&&(_p$.a=_$setProp7(_el$5,"style",_v$3,_p$.a))
592
797
  return _p$
593
798
  },{e:undefined,t:undefined,a:undefined
594
799
  })
595
800
  return _el$2
596
- })(),(()=>{var _el$7=_$createElement4("box"),_el$8=_$createElement4("text")
597
- _$insertNode2(_el$7,_el$8)
598
- _$setProp4(_el$7,"flexDirection","row")
599
- _$setProp4(_el$7,"gap",1)
600
- _$setProp4(_el$7,"flexGrow",1)
601
- _$setProp4(_el$7,"minWidth",0)
602
- _$setProp4(_el$7,"justifyContent","flex-end")
603
- _$insert4(_el$7,_$createComponent6(Show2,{when:action,children:(control)=>_$createComponent6(SelectionBox2,{ref:(node)=>control().ref(node),get id(){return props.headerAction?.id
801
+ })(),(()=>{var _el$7=_$createElement7("box"),_el$8=_$createElement7("text")
802
+ _$insertNode4(_el$7,_el$8)
803
+ _$setProp7(_el$7,"flexDirection","row")
804
+ _$setProp7(_el$7,"gap",1)
805
+ _$setProp7(_el$7,"flexGrow",1)
806
+ _$setProp7(_el$7,"minWidth",0)
807
+ _$setProp7(_el$7,"justifyContent","flex-end")
808
+ _$insert6(_el$7,_$createComponent9(Show3,{when:action,children:(control)=>_$createComponent9(SelectionBox2,{ref:(node)=>control().ref(node),get id(){return props.headerAction?.id
604
809
  },flexShrink:1,minWidth:3,overflow:"hidden",get backgroundColor(){return control().backgroundColor()
605
810
  },onSizeChange:function(){setActionWidth(this.width)
606
811
  },get onMouseOver(){return control().onMouseOver
607
812
  },get onMouseOut(){return control().onMouseOut
608
813
  },onMouseDown:(event)=>event.stopPropagation(),onMouseUp:(event)=>{event.stopPropagation()
609
814
  control().activate(event)
610
- },get children(){var _el$9=_$createElement4("text")
611
- _$setProp4(_el$9,"wrapMode","none")
612
- _$setProp4(_el$9,"height",1)
613
- _$insert4(_el$9,actionLabel)
614
- _$effect4((_$p)=>_$setProp4(_el$9,"fg",control().foregroundColor(),_$p))
815
+ },get children(){var _el$9=_$createElement7("text")
816
+ _$setProp7(_el$9,"wrapMode","none")
817
+ _$setProp7(_el$9,"height",1)
818
+ _$insert6(_el$9,actionLabel)
819
+ _$effect7((_$p)=>_$setProp7(_el$9,"fg",control().foregroundColor(),_$p))
615
820
  return _el$9
616
821
  }})
617
822
  }),_el$8)
618
- _$setProp4(_el$8,"wrapMode","none")
619
- _$setProp4(_el$8,"truncate",true)
620
- _$setProp4(_el$8,"height",1)
621
- _$insert4(_el$8,()=>props.summary)
622
- _$effect4((_p$)=>{var _v$4=item.focused()?item.foregroundColor():theme().textMuted,_v$5=props.headerAction?0:1
623
- _v$4!==_p$.e&&(_p$.e=_$setProp4(_el$8,"fg",_v$4,_p$.e))
624
- _v$5!==_p$.t&&(_p$.t=_$setProp4(_el$8,"flexShrink",_v$5,_p$.t))
823
+ _$setProp7(_el$8,"wrapMode","none")
824
+ _$setProp7(_el$8,"truncate",true)
825
+ _$setProp7(_el$8,"height",1)
826
+ _$insert6(_el$8,()=>props.summary)
827
+ _$effect7((_p$)=>{var _v$4=item.focused()?item.foregroundColor():theme().textMuted,_v$5=props.headerAction?0:1
828
+ _v$4!==_p$.e&&(_p$.e=_$setProp7(_el$8,"fg",_v$4,_p$.e))
829
+ _v$5!==_p$.t&&(_p$.t=_$setProp7(_el$8,"flexShrink",_v$5,_p$.t))
625
830
  return _p$
626
831
  },{e:undefined,t:undefined
627
832
  })
628
833
  return _el$7
629
834
  })()]
630
835
  }}),null)
631
- _$insert4(_el$,_$createComponent6(Show2,{get when(){return props.open
836
+ _$insert6(_el$,_$createComponent9(Show3,{get when(){return props.open
632
837
  },get children(){return props.children
633
838
  }}),null)
634
839
  return _el$
635
840
  })()
636
841
  }
637
- function ProviderLimitsSection(props){const icons=useIcons2()
638
- const dialogs=useDialogs2(props.api)
842
+ function ProviderLimitsSection(props){const icons=useIcons3()
843
+ const dialogs=useDialogs4(props.api)
639
844
  const model=()=>props.controller.current().model
640
845
  const snapshot=props.controller.snapshot
641
- const accountLink=createCodexAccountLink({api:props.api,preferences:props.preferences,adapter:props.codex,model,dialogs
642
- })
643
- const action=(id,row,label,onActivate,disabled=false)=>_$createComponent7(LimitsAction,{get api(){return props.api
846
+ const count=()=>(snapshot()?.windows.length??0) + (snapshot()?.balances?.length??0)
847
+ const action=(id,row,label,onActivate,disabled=false)=>_$createComponent10(LimitsAction,{get api(){return props.api
644
848
  },get interaction(){return props.interaction
645
849
  },id:`${PLUGIN_ID}.limits.${id}`,get position(){return{section:props.navigationSection,row,column:0
646
850
  }},label,onActivate,disabled
647
851
  })
648
852
  function openCredits(){const selected=model()
649
- if(!selected)
650
- return
651
- dialogs.open(()=>_$createComponent7(ResetCreditDialog,{get api(){return props.api
853
+ if(selected)
854
+ dialogs.open(()=>_$createComponent10(ResetCreditDialog,{get api(){return props.api
652
855
  },get preferences(){return props.preferences
653
856
  },get controller(){return props.controller
654
857
  },get adapter(){return props.codex
655
858
  },model:selected
656
859
  }))
657
860
  }
658
- return _$createComponent7(Section,{get api(){return props.api
861
+ return _$createComponent10(Section,{get api(){return props.api
659
862
  },get interaction(){return props.interaction
660
863
  },sectionId:`${PLUGIN_ID}.section.limits`,get navigationSection(){return props.navigationSection
661
- },title:"LIMITS",section:"limits",get summary(){return _$memo3(()=>snapshot()?.availability==="ready")()?String(snapshot()?.windows.length):""
864
+ },title:"LIMITS",section:"limits",get summary(){return _$memo6(()=>!!count())()?String(count()):""
662
865
  },get open(){return props.preferences.expanded().limits
663
- },onToggle:()=>props.preferences.toggleSectionExpanded("limits"),get children(){return _$createComponent7(Show3,{get when(){return model()
664
- },get fallback(){return(()=>{var _el$=_$createElement5("text")
665
- _$insertNode3(_el$,_$createTextNode3(`Open a session to view model limits`))
666
- _$effect5((_$p)=>_$setProp5(_el$,"fg",props.api.theme.current.textMuted,_$p))
866
+ },onToggle:()=>props.preferences.toggleSectionExpanded("limits"),get children(){return _$createComponent10(Show4,{get when(){return model()
867
+ },get fallback(){return(()=>{var _el$=_$createElement8("text")
868
+ _$insertNode5(_el$,_$createTextNode4(`Open a session to view model limits`))
869
+ _$effect8((_$p)=>_$setProp8(_el$,"fg",props.api.theme.current.textMuted,_$p))
667
870
  return _el$
668
871
  })()
669
- },children:(selected)=>(()=>{var _el$3=_$createElement5("box"),_el$4=_$createElement5("text"),_el$5=_$createTextNode3(`/`)
670
- _$insertNode3(_el$3,_el$4)
671
- _$setProp5(_el$3,"gap",1)
672
- _$insertNode3(_el$4,_el$5)
673
- _$setProp5(_el$4,"wrapMode","none")
674
- _$setProp5(_el$4,"truncate",true)
675
- _$insert5(_el$4,()=>selected().providerID,_el$5)
676
- _$insert5(_el$4,()=>selected().modelID,null)
677
- _$insert5(_el$3,_$createComponent7(Show3,{get when(){return _$memo3(()=>!!!selected().accountID)()&&selected().providerID==="openai"
678
- },get children(){return [(()=>{var _el$6=_$createElement5("text")
679
- _$insertNode3(_el$6,_$createTextNode3(`Link the matching Codex account to read quotas`))
680
- _$effect5((_$p)=>_$setProp5(_el$6,"fg",props.api.theme.current.textMuted,_$p))
872
+ },children:(selected)=>(()=>{var _el$3=_$createElement8("box"),_el$4=_$createElement8("text"),_el$5=_$createTextNode4(`/`)
873
+ _$insertNode5(_el$3,_el$4)
874
+ _$setProp8(_el$3,"gap",1)
875
+ _$insertNode5(_el$4,_el$5)
876
+ _$setProp8(_el$4,"wrapMode","none")
877
+ _$setProp8(_el$4,"truncate",true)
878
+ _$insert7(_el$4,()=>selected().providerID,_el$5)
879
+ _$insert7(_el$4,()=>selected().modelID,null)
880
+ _$insert7(_el$3,_$createComponent10(CodexAccountControls,_$mergeProps(props,{get model(){return selected()
881
+ }})),null)
882
+ _$insert7(_el$3,_$createComponent10(Show4,{get when(){return _$memo6(()=>!!(snapshot()&&["ready","stale"].includes(snapshot().availability)))()?snapshot():undefined
883
+ },children:(quota)=>_$createComponent10(QuotaSnapshotRows,{get api(){return props.api
884
+ },get snapshot(){return quota()
885
+ }})
886
+ }),null)
887
+ _$insert7(_el$3,_$createComponent10(Show4,{get when(){return snapshot()?.bankedResets
888
+ },children:(credits)=>[(()=>{var _el$6=_$createElement8("text"),_el$7=_$createTextNode4(`Banked resets: `)
889
+ _$insertNode5(_el$6,_el$7)
890
+ _$insert7(_el$6,()=>credits().availableCount,null)
891
+ _$effect8((_$p)=>_$setProp8(_el$6,"fg",props.api.theme.current.textMuted,_$p))
681
892
  return _el$6
682
- })(),_$memo3(()=>action("link",1,`${icons.icon("scope")} Link Codex CLI account`,()=>void accountLink.link(),accountLink.busy()))]
683
- }}),null)
684
- _$insert5(_el$3,_$createComponent7(Show3,{get when(){return _$memo3(()=>!!selected().accountID)()&&snapshot()?.availability==="ready"
685
- },get children(){return [(()=>{var _el$8=_$createElement5("text"),_el$9=_$createTextNode3(`Codex CLI \xB7 `)
686
- _$insertNode3(_el$8,_el$9)
687
- _$setProp5(_el$8,"wrapMode","none")
688
- _$setProp5(_el$8,"truncate",true)
689
- _$insert5(_el$8,()=>snapshot()?.accountId,null)
690
- _$effect5((_$p)=>_$setProp5(_el$8,"fg",props.api.theme.current.textMuted,_$p))
691
- return _el$8
692
- })(),_$createComponent7(For,{get each(){return snapshot()?.windows
693
- },children:(window)=>_$createComponent7(QuotaWindowRow,{get api(){return props.api
694
- },window
695
- })
696
- }),_$createComponent7(Show3,{get when(){return snapshot()?.bankedResets
697
- },children:(credits)=>[(()=>{var _el$10=_$createElement5("text"),_el$11=_$createTextNode3(`Banked resets: `)
698
- _$insertNode3(_el$10,_el$11)
699
- _$insert5(_el$10,()=>credits().availableCount,null)
700
- _$effect5((_$p)=>_$setProp5(_el$10,"fg",props.api.theme.current.textMuted,_$p))
701
- return _el$10
702
- })(),_$createComponent7(Show3,{get when(){return credits().availableCount>0||props.preferences.pendingResetAttempt()
893
+ })(),_$createComponent10(Show4,{get when(){return credits().availableCount>0||props.preferences.pendingResetAttempt()
703
894
  },get children(){return action("credits",2,`${icons.icon("info")} Review reset credits`,openCredits)
704
895
  }})]
705
- })]
706
- }}),null)
707
- _$insert5(_el$3,_$createComponent7(LimitsStatus,{get api(){return props.api
896
+ }),null)
897
+ _$insert7(_el$3,_$createComponent10(LimitsStatus,{get api(){return props.api
708
898
  },get controller(){return props.controller
709
899
  }}),null)
710
- _$insert5(_el$3,_$createComponent7(Show3,{get when(){return selected().providerID!=="openai"
711
- },get children(){var _el$0=_$createElement5("text")
712
- _$insertNode3(_el$0,_$createTextNode3(`No documented quota API for this model`))
713
- _$effect5((_$p)=>_$setProp5(_el$0,"fg",props.api.theme.current.textMuted,_$p))
714
- return _el$0
715
- }}),null)
716
- _$insert5(_el$3,_$createComponent7(Show3,{get when(){return props.controller.current().adapter
900
+ _$insert7(_el$3,_$createComponent10(Show4,{get when(){return props.controller.current().adapter||selected().hostConnection?.status==="unavailable"
717
901
  },get children(){return action("refresh",3,`${icons.icon("retry")} ${props.controller.state().status === "error" ? "Retry" : "Refresh"}`,()=>void props.controller.refresh(true),["loading","refreshing"].includes(props.controller.state().status))
718
902
  }}),null)
719
- _$insert5(_el$3,_$createComponent7(Show3,{get when(){return _$memo3(()=>selected().providerID==="openai")()&&selected().accountID
720
- },get children(){return [_$memo3(()=>action("unlink",4,`${icons.icon("close")} Unlink Codex account`,()=>props.preferences.setCodexAccountBinding(selected().providerID,selected().modelID))),_$createComponent7(Show3,{get when(){return snapshot()?.availability==="unauthenticated"
721
- },get children(){return action("relink",5,`${icons.icon("scope")} Relink Codex account`,()=>void accountLink.link(),accountLink.busy())
722
- }})]
723
- }}),null)
724
- _$effect5((_$p)=>_$setProp5(_el$4,"fg",props.api.theme.current.textMuted,_$p))
903
+ _$insert7(_el$3,()=>action("sources",6,`${icons.icon("info")} Provider sources`,()=>dialogs.open(()=>_$createComponent10(ProviderSupportDialog,{get api(){return props.api
904
+ },get load(){return props.capabilities
905
+ }}))),null)
906
+ _$effect8((_$p)=>_$setProp8(_el$4,"fg",props.api.theme.current.textMuted,_$p))
725
907
  return _el$3
726
908
  })()
727
909
  })
@@ -735,5 +917,111 @@ const model=api.state.session.get(sessionID)?.model
735
917
  if(!model)
736
918
  return
737
919
  return{sessionID,providerID:model.providerID,modelID:model.id,...model.variant&&{variant:model.variant}}}
738
- export{ProviderLimitsSection,createCodexAppServerClient,createCodexQuotaAdapter,createProviderLimitsController,selectedV1Model
920
+ import {createSignal as createSignal7,untrack as untrack2} from "solid-js"
921
+ var CHATGPT_METHODS=new Set(["chatgpt-browser","chatgpt-headless"])
922
+ function providerAccountIdentity(provider,credentials){if(!provider.integrationID||provider.activation==="disabled")
923
+ return{status:"unsupported"}
924
+ const active=credentials.filter((entry)=>entry.integrationID===provider.integrationID&&entry.active)
925
+ if(active.length!==1)
926
+ return{status:"unavailable"}
927
+ const credential=active[0]
928
+ const id=`credential:${credential.id}`
929
+ const providerID=provider.canonical??provider.id
930
+ if(credential.value.type==="key")
931
+ return{status:"ready",id,method:"api",providerID}
932
+ if(provider.integrationID!=="openai")
933
+ return{status:"ready",id,method:"oauth",providerID}
934
+ if(!CHATGPT_METHODS.has(credential.value.methodID))
935
+ return{status:"unsupported",id}
936
+ const account=credential.value.metadata?.accountID
937
+ const accountID=typeof account==="string"&&account.trim()?account:undefined
938
+ const header=Object.entries(provider.headers??{}).find(([name])=>name.toLowerCase()==="chatgpt-account-id")?.[1]
939
+ if(accountID&&header&&header!==accountID)
940
+ return{status:"loading"}
941
+ return{status:"ready",id,method:"oauth",providerID,...accountID&&{accountID}}}
942
+ function createOpenCodeV2AccountSource(context,lifecycle){const [connections,setConnections]=createSignal7({})
943
+ let generation=0
944
+ let disposed=false
945
+ let lastModel
946
+ let pending
947
+ const location=()=>context.location??context.data.location.default()
948
+ const key=(model)=>JSON.stringify([model.providerID,location()])
949
+ function save(target,connection){if(JSON.stringify(connections()[target])!==JSON.stringify(connection))
950
+ setConnections((values)=>({...values,[target]:connection}))
951
+ }
952
+ async function read(model,signal=lifecycle){if(disposed||signal.aborted)
953
+ return{status:"unavailable"}
954
+ const target=key(model)
955
+ const epoch=generation
956
+ if(pending?.key===target&&pending.generation===epoch)
957
+ return pending.promise
958
+ const promise=(async ()=>{try{if(!context.client.provider?.get||!context.client.credential?.list)
959
+ throw new Error("Account metadata unavailable")
960
+ const options={signal:AbortSignal.any([lifecycle,signal])}
961
+ const [provider,credentials]=await Promise.all([context.client.provider.get({providerID:model.providerID,location:location()},options),context.client.credential.list(options)])
962
+ let connection=provider.data.id===model.providerID?providerAccountIdentity(provider.data,credentials):{status:"unavailable"}
963
+ if(connection.status==="unavailable"&&credentials.every((entry)=>!(entry.integrationID===provider.data.integrationID&&entry.active))&&provider.data.integrationID&&typeof context.client.integration?.get==="function"){const integration=await context.client.integration.get({integrationID:provider.data.integrationID,location:location()},options)
964
+ const active=integration.data.connections[0]
965
+ if(active?.type==="env"&&!active.status)
966
+ connection={status:"ready",id:`env:${active.name}`,method:"api",providerID:provider.data.canonical??provider.data.id
967
+ }}
968
+ if(disposed||signal.aborted||epoch!==generation||key(model)!==target)
969
+ return{status:"unavailable"}
970
+ save(target,connection)
971
+ return connection
972
+ } catch{if(!disposed&&!signal.aborted&&epoch===generation)
973
+ save(target,{status:"unavailable"})
974
+ return{status:"unavailable"}}})()
975
+ pending={key:target,generation:epoch,promise}
976
+ try{return await promise
977
+ } finally{if(pending?.promise===promise)
978
+ pending=undefined
979
+ }}
980
+ function invalidate(){generation++
981
+ setConnections({})
982
+ if(lastModel&&!disposed)
983
+ read(lastModel)
984
+ }
985
+ const events=["credential.switched","credential.updated","integration.updated","provider.updated","server.connected"]
986
+ const unsubscribes=events.map((event)=>context.data.on(event,()=>untrack2(invalidate)))
987
+ return{current(model){lastModel=model
988
+ return{...model,hostConnection:connections()[key(model)]??{status:"loading"}}},async refresh(model){await read(model)
989
+ },async validate(model,signal){const actual=await read(model,signal)
990
+ const expected=model.hostConnection
991
+ return actual.status==="ready"&&expected?.status==="ready"&&actual.id===expected.id&&actual.accountID===expected.accountID&&(!actual.accountID||actual.accountID===model.accountID)
992
+ },dispose(){disposed=true
993
+ generation++
994
+ for(const unsubscribe of unsubscribes)
995
+ unsubscribe()
996
+ }}}
997
+ function bindCodexAccount(model,preferences){const connection=model.hostConnection
998
+ if(model.providerID!=="openai"&&connection?.providerID!=="openai"||connection?.method==="api")
999
+ return{...model,accountID:undefined,accountSource:undefined}
1000
+ if(connection&&connection.status!=="ready")
1001
+ return{...model,accountID:undefined,accountSource:undefined}
1002
+ const accountID=connection?.accountID??preferences.codexAccountForConnection(model.providerID,connection?.id,model.modelID)
1003
+ const accountSource=connection?.accountID?"host":"binding"
1004
+ return{...model,accountID,accountSource:accountID?accountSource:undefined}}
1005
+ function selectedV2Model(context){const route=context.ui.router.current()
1006
+ const model=context.ui.model.current()
1007
+ if(route.type!=="session"||!model)
1008
+ return
1009
+ return{sessionID:route.sessionID,providerID:model.providerID,modelID:model.modelID,...model.variant&&{variant:model.variant}}}
1010
+ function createProviderQuotaIntegration(api,preferences,version,context){const selectModel=()=>context?selectedV2Model(context):selectedV1Model(api)
1011
+ const accountSource=context?createOpenCodeV2AccountSource(context,api.lifecycle.signal):undefined
1012
+ const codex=createCodexQuotaAdapter(createCodexAppServerClient({version}),accountSource?.validate)
1013
+ const native=context?createNativeQuotaAdapter(context):undefined
1014
+ const controller=createProviderLimitsController([codex,...native?[native.adapter]:[]],()=>{const model=selectModel()
1015
+ return model?bindCodexAccount(accountSource?.current(model)??model,preferences):undefined
1016
+ },api.lifecycle.signal,accountSource&&(async ()=>{const model=selectModel()
1017
+ if(model)
1018
+ await accountSource.refresh(model)
1019
+ }))
1020
+ native?.onUpdated((providerID,modelID)=>{const selected=selectModel()
1021
+ if(selected?.providerID===providerID&&selected.modelID===modelID)
1022
+ controller.refresh()
1023
+ })
1024
+ return{controller,codex,accountSource,selectModel,capabilities:native?.capabilities??(async ()=>providerQuotaCatalog())
1025
+ }}
1026
+ export{ProviderLimitsSection,bindCodexAccount,createCodexAppServerClient,createCodexQuotaAdapter,createOpenCodeV2AccountSource,createProviderLimitsController,createProviderQuotaIntegration,selectedV1Model
739
1027
  }