opencode-navigator 0.20.0 → 0.21.1
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/CHANGELOG.md +14 -0
- package/PROVIDERS.md +92 -0
- package/README.md +56 -26
- package/dist/provider-limits.js +489 -187
- package/dist/rpc.js +6 -1
- package/dist/server.js +589 -1
- package/dist/tui.js +124 -79
- package/package.json +13 -10
- package/scripts/update-local-snapshot.mjs +14 -12
package/dist/server.js
CHANGED
|
@@ -43,6 +43,592 @@ var NavigatorTodoRpc={id:"opencode-navigator.todo",methods:{list:{input:{type:"o
|
|
|
43
43
|
},output
|
|
44
44
|
}},events:{updated:{schema:{type:"object",properties:{sessionID,todos,guidance:{type:"boolean"}},required:["sessionID","todos","guidance"],additionalProperties:false
|
|
45
45
|
}}}};
|
|
46
|
+
var text={type:"string",minLength:1,maxLength:512}
|
|
47
|
+
var model={type:"object",properties:{providerID:text,modelID:text,sessionID:text,connectionID:text,variant:text,force:{type:"boolean"}},required:["providerID","modelID","sessionID"],additionalProperties:false
|
|
48
|
+
}
|
|
49
|
+
var NavigatorQuotaRpc={id:"opencode-navigator.quotas",methods:{read:{input:model,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
|
|
50
|
+
}}}};
|
|
51
|
+
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")
|
|
52
|
+
}
|
|
53
|
+
const windows=snapshot.windows.map((window)=>{if(!window.id||!window.label||window.scope!==undefined&&window.scope!=="account"&&window.scope!=="model"||(window.scope==="account"?!snapshot.accountId||window.modelID!==undefined: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")
|
|
54
|
+
}
|
|
55
|
+
return{...window}})
|
|
56
|
+
const bankedResets=snapshot.bankedResets
|
|
57
|
+
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))
|
|
58
|
+
throw new Error("Invalid provider balance")
|
|
59
|
+
return{...balance}})
|
|
60
|
+
if(bankedResets&&(!Number.isSafeInteger(bankedResets.availableCount)||bankedResets.availableCount<0)){throw new Error("Invalid banked reset count")
|
|
61
|
+
}
|
|
62
|
+
return{...snapshot,windows,...balances&&{balances},...bankedResets&&{bankedResets:{availableCount:bankedResets.availableCount,...bankedResets.credits&&{credits:[...bankedResets.credits]}}}}}
|
|
63
|
+
var rows=[["openai","OpenAI / Codex",["subscription","api-rate-limits"],"Verified Codex account quota shared across models; 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"]]
|
|
64
|
+
var PROVIDER_QUOTA_CAPABILITIES= rows.map(([id,name,sources,reason,url])=>({id,name,sources,reason,documentation:[url]}))
|
|
65
|
+
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"
|
|
66
|
+
}
|
|
67
|
+
function providerQuotaCapability(id,canonical){const key=aliases[canonical??id]??canonical??id
|
|
68
|
+
const found=PROVIDER_QUOTA_CAPABILITIES.find((entry)=>entry.id===key)
|
|
69
|
+
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/"]
|
|
70
|
+
}}
|
|
71
|
+
function providerQuotaCatalog(providers=[]){const entries=new Map(PROVIDER_QUOTA_CAPABILITIES.map((entry)=>[entry.id,entry]))
|
|
72
|
+
for(const provider of providers)
|
|
73
|
+
entries.set(provider.id,{...providerQuotaCapability(provider.id,provider.canonical),id:provider.id,name:provider.name
|
|
74
|
+
})
|
|
75
|
+
return [...entries.values()].sort((a,b)=>a.name.localeCompare(b.name))
|
|
76
|
+
}
|
|
77
|
+
import {createHash} from "crypto"
|
|
78
|
+
import {mkdtemp,rm} from "fs/promises"
|
|
79
|
+
import {tmpdir} from "os"
|
|
80
|
+
import {join} from "path"
|
|
81
|
+
function object(value){return value&&typeof value==="object"&&!Array.isArray(value)?value:{}}
|
|
82
|
+
function number(value){if(typeof value==="number"&&Number.isFinite(value))
|
|
83
|
+
return value
|
|
84
|
+
return typeof value==="string"&&/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value)&&Number.isFinite(Number(value))?Number(value):undefined
|
|
85
|
+
}
|
|
86
|
+
function balance(id,label,value,unit,scope="account"){if(number(value)===undefined)
|
|
87
|
+
return []
|
|
88
|
+
return [{id,label,amount:String(value),...unit&&{unit},scope}]
|
|
89
|
+
}
|
|
90
|
+
function scaledAmount(value,digits,multiplier=1n){const numeric=typeof value==="number"&&Number.isSafeInteger(value)?String(value):""
|
|
91
|
+
const text=typeof value==="string"?value:numeric
|
|
92
|
+
if(!/^-?\d+$/.test(text))
|
|
93
|
+
return
|
|
94
|
+
const raw=BigInt(text)*multiplier
|
|
95
|
+
const negative=raw<0n
|
|
96
|
+
const absolute=negative?-raw:raw
|
|
97
|
+
const divisor=10n ** BigInt(digits)
|
|
98
|
+
return `${negative ? "-" : ""}${absolute / divisor}.${String(absolute % divisor).padStart(digits, "0")}`
|
|
99
|
+
}
|
|
100
|
+
function milliseconds(value){const time=number(value)
|
|
101
|
+
return time!==undefined&&time>=100000000000?time / 1000:undefined
|
|
102
|
+
}
|
|
103
|
+
async function withinQuotaSignal(signal,work){signal.throwIfAborted()
|
|
104
|
+
let abort
|
|
105
|
+
try{return await Promise.race([new Promise((_,reject)=>{abort=()=>reject(signal.reason)
|
|
106
|
+
signal.addEventListener("abort",abort,{once:true})
|
|
107
|
+
}),Promise.try(()=>{signal.throwIfAborted()
|
|
108
|
+
return work()
|
|
109
|
+
})])
|
|
110
|
+
} finally{if(abort)
|
|
111
|
+
signal.removeEventListener("abort",abort)
|
|
112
|
+
}}
|
|
113
|
+
class QuotaReadError extends Error{status
|
|
114
|
+
constructor(status,message){super(message)
|
|
115
|
+
this.status=status
|
|
116
|
+
this.name="QuotaReadError"
|
|
117
|
+
}}
|
|
118
|
+
async function readProviderJson(fetcher,url,token,signal,input={}){const combined=AbortSignal.any([signal,AbortSignal.timeout(1e4)])
|
|
119
|
+
const response=await withinQuotaSignal(combined,()=>fetcher(url,{method:input.method??"GET",headers:{...input.apiKeyHeader?{[input.apiKeyHeader]:token}:{Authorization:`Bearer ${token}`},Accept:"application/json",...input.headers
|
|
120
|
+
},signal:combined,redirect:"error"
|
|
121
|
+
}))
|
|
122
|
+
if(!response.ok)
|
|
123
|
+
throw new QuotaReadError(response.status,"Provider quota read was rejected")
|
|
124
|
+
return readQuotaResponseBody(response,combined)
|
|
125
|
+
}
|
|
126
|
+
async function readQuotaResponseBody(response,signal=AbortSignal.timeout(1e4)){const reader=response.body?.getReader()
|
|
127
|
+
const decoder=new TextDecoder
|
|
128
|
+
let body=""
|
|
129
|
+
let bytes=0
|
|
130
|
+
if(reader){try{while(true){const next=await withinQuotaSignal(signal,()=>reader.read())
|
|
131
|
+
if(next.done)
|
|
132
|
+
break
|
|
133
|
+
bytes += next.value.byteLength
|
|
134
|
+
if(bytes>1e6)
|
|
135
|
+
throw new QuotaReadError(502,"Provider quota response is too large")
|
|
136
|
+
body += decoder.decode(next.value,{stream:true})
|
|
137
|
+
}
|
|
138
|
+
body += decoder.decode()
|
|
139
|
+
} finally{reader.cancel().catch(()=>{})
|
|
140
|
+
reader.releaseLock()
|
|
141
|
+
}}
|
|
142
|
+
try{return object(JSON.parse(body))
|
|
143
|
+
} catch{throw new QuotaReadError(502,"Provider quota response is invalid")
|
|
144
|
+
}}
|
|
145
|
+
function createCopilotRuntime(load,timeoutMs){let active
|
|
146
|
+
let disposed=false
|
|
147
|
+
async function cleanup(runtime){const client=runtime.client
|
|
148
|
+
const directory=runtime.directory
|
|
149
|
+
runtime.client=undefined
|
|
150
|
+
runtime.directory=undefined
|
|
151
|
+
await withinQuotaSignal(AbortSignal.timeout(timeoutMs),async ()=>client?.forceStop()).catch(()=>{})
|
|
152
|
+
if(directory)
|
|
153
|
+
await rm(directory,{recursive:true,force:true}).catch(()=>{})
|
|
154
|
+
}
|
|
155
|
+
function discard(runtime){if(active===runtime)
|
|
156
|
+
active=undefined
|
|
157
|
+
runtime.controller.abort(new QuotaReadError(503,"Copilot quota runtime stopped"))
|
|
158
|
+
return cleanup(runtime)
|
|
159
|
+
}
|
|
160
|
+
async function start(runtime){const signal=AbortSignal.any([runtime.controller.signal,AbortSignal.timeout(timeoutMs)])
|
|
161
|
+
try{return await withinQuotaSignal(signal,async ()=>{const sdk=await load()
|
|
162
|
+
signal.throwIfAborted()
|
|
163
|
+
const directory=await mkdtemp(join(tmpdir(),"navigator-copilot-quotas-"))
|
|
164
|
+
runtime.directory=directory
|
|
165
|
+
if(signal.aborted){await cleanup(runtime)
|
|
166
|
+
signal.throwIfAborted()
|
|
167
|
+
}
|
|
168
|
+
const client=new sdk.CopilotClient({mode:"empty",baseDirectory:directory,useLoggedInUser:false,logLevel:"none",enableRemoteSessions:false
|
|
169
|
+
})
|
|
170
|
+
runtime.client=client
|
|
171
|
+
try{await client.start()
|
|
172
|
+
} finally{if(signal.aborted){runtime.client=client
|
|
173
|
+
runtime.directory=directory
|
|
174
|
+
await cleanup(runtime)
|
|
175
|
+
}}
|
|
176
|
+
signal.throwIfAborted()
|
|
177
|
+
return client
|
|
178
|
+
})
|
|
179
|
+
} catch{await discard(runtime)
|
|
180
|
+
throw new QuotaReadError(503,"The official Copilot SDK quota runtime is unavailable")
|
|
181
|
+
}}
|
|
182
|
+
return{async use(signal,work){signal.throwIfAborted()
|
|
183
|
+
if(disposed)
|
|
184
|
+
throw new QuotaReadError(503,"Copilot quota reader disposed")
|
|
185
|
+
const runtime=active ??={controller:new AbortController}
|
|
186
|
+
const combined=AbortSignal.any([signal,runtime.controller.signal,AbortSignal.timeout(timeoutMs)])
|
|
187
|
+
runtime.startup ??= start(runtime)
|
|
188
|
+
try{return await withinQuotaSignal(combined,async ()=>{const client=await runtime.startup
|
|
189
|
+
combined.throwIfAborted()
|
|
190
|
+
return work(client)
|
|
191
|
+
})
|
|
192
|
+
} catch(error){discard(runtime)
|
|
193
|
+
throw error
|
|
194
|
+
}},async dispose(){disposed=true
|
|
195
|
+
if(active)
|
|
196
|
+
await discard(active)
|
|
197
|
+
}}}
|
|
198
|
+
function createCopilotQuotaReader(load=()=>import("@github/copilot-sdk"),timeoutMs=1e4){const runtime=createCopilotRuntime(load,timeoutMs)
|
|
199
|
+
return{async read(input){const [catalog,quota]=await runtime.use(input.signal,(sdk)=>Promise.all([sdk.rpc.models.list({gitHubToken:input.token}),sdk.rpc.account.getQuota({gitHubToken:input.token})]))
|
|
200
|
+
const selected=catalog.models.find((model)=>model.id===input.apiModelID)
|
|
201
|
+
const premium=quota.quotaSnapshots.premium_interactions
|
|
202
|
+
const base={model:input.model,providerId:input.model.providerID,providerName:input.provider.name,fetchedAt:Date.now(),windows:[]
|
|
203
|
+
}
|
|
204
|
+
input.signal.throwIfAborted()
|
|
205
|
+
if(!selected||!selected.billing||!selected.billing.tokenPrices&&(selected.billing.multiplier??0)<=0)
|
|
206
|
+
return{...base,availability:"unsupported",message:"Copilot did not associate the selected model with a premium billing allowance."
|
|
207
|
+
}
|
|
208
|
+
if(!premium)
|
|
209
|
+
return{...base,availability:"unavailable",message:"Copilot did not report a premium allowance; missing details are not zero."
|
|
210
|
+
}
|
|
211
|
+
if(premium.isUnlimitedEntitlement)
|
|
212
|
+
return{...base,availability:"ready",message:"Copilot reports an unlimited premium entitlement for this account."
|
|
213
|
+
}
|
|
214
|
+
const reset=premium.resetDate?Date.parse(premium.resetDate) / 1000:undefined
|
|
215
|
+
if(!Number.isFinite(premium.remainingPercentage))
|
|
216
|
+
return{...base,availability:"unavailable",message:"Copilot quota percentage is unavailable."}
|
|
217
|
+
return{...base,availability:"ready",windows:[{id:"copilot-premium",label:"Premium allowance",modelID:input.model.modelID,kind:"subscription",unit:"%",used:100-premium.remainingPercentage,remaining:premium.remainingPercentage,...reset!==undefined&&Number.isFinite(reset)&&{resetsAt:reset}}]
|
|
218
|
+
}},dispose:runtime.dispose
|
|
219
|
+
}}
|
|
220
|
+
function connectionKey(connection){return connection.type==="credential"?`credential:${connection.id}`:`env:${connection.name}`
|
|
221
|
+
}
|
|
222
|
+
function nativeBaseURL(provider){return typeof provider.settings?.baseURL==="string"?provider.settings.baseURL:undefined
|
|
223
|
+
}
|
|
224
|
+
function matchingOrigin(provider,origins){const configured=nativeBaseURL(provider)
|
|
225
|
+
if(!configured)
|
|
226
|
+
return true
|
|
227
|
+
try{return origins.includes(new URL(configured).origin)
|
|
228
|
+
} catch{return false
|
|
229
|
+
}}
|
|
230
|
+
async function readFireworksQuota(input){if(!matchingOrigin(input.provider,["https://api.fireworks.ai"]))
|
|
231
|
+
return
|
|
232
|
+
const read=(url)=>readProviderJson(input.fetcher,url,input.token,input.signal)
|
|
233
|
+
const accounts=await read("https://api.fireworks.ai/v1/accounts?pageSize=200&readMask=name,displayName")
|
|
234
|
+
const candidates=Array.isArray(accounts.accounts)?accounts.accounts.map((entry)=>object(entry)):[]
|
|
235
|
+
const requested=input.credential.metadata?.accountID
|
|
236
|
+
const selected=typeof requested==="string"?candidates.filter((entry)=>entry.name===requested||entry.name===`accounts/${requested}`):candidates
|
|
237
|
+
const base={model:input.model,providerId:input.model.providerID,providerName:input.provider.name,fetchedAt:Date.now(),windows:[]
|
|
238
|
+
}
|
|
239
|
+
if(selected.length!==1||requested===undefined&&(accounts.nextPageToken||number(accounts.totalSize)>1))
|
|
240
|
+
return{...base,availability:"unavailable",message:"Fireworks billing account is ambiguous; it is never inferred from a model owner."
|
|
241
|
+
}
|
|
242
|
+
const name=typeof selected[0].name==="string"?selected[0].name:""
|
|
243
|
+
if(!/^accounts\/[\w-]+$/.test(name))
|
|
244
|
+
return{...base,availability:"unavailable",message:"Fireworks did not report an account resource name."}
|
|
245
|
+
let page
|
|
246
|
+
for(let index=0;index<10; index++){const url=new URL(`https://api.fireworks.ai/v1/${name}/quotas`)
|
|
247
|
+
url.searchParams.set("pageSize","200")
|
|
248
|
+
if(page)
|
|
249
|
+
url.searchParams.set("pageToken",page)
|
|
250
|
+
const body=await read(url.href)
|
|
251
|
+
const quotas=Array.isArray(body.quotas)?body.quotas:[]
|
|
252
|
+
for(const raw of quotas){const quota=object(raw)
|
|
253
|
+
const total=number(quota.value)
|
|
254
|
+
const used=number(quota.usage)
|
|
255
|
+
if(quota.name===`${name}/quotas/monthly-spend-usd`&&total!==undefined)
|
|
256
|
+
base.windows.push({id:"fireworks-monthly-spend",label:"Account monthly spend cap",modelID:input.model.modelID,limitID:quota.name,kind:"configured_limit",unit:"USD",total,...used!==undefined&&{used,remaining:total-used}})
|
|
257
|
+
}
|
|
258
|
+
page=typeof body.nextPageToken==="string"&&body.nextPageToken?body.nextPageToken:undefined
|
|
259
|
+
if(!page)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
return{...base,accountId:name,availability:base.windows.length>0?"ready":"unavailable",message:"Account-wide spend quota. GPU and unrelated deployment quotas are excluded."
|
|
263
|
+
}}
|
|
264
|
+
function googleQuotaFailureWindows(body,modelID,apiModelID){const details=object(body.error).details
|
|
265
|
+
return(Array.isArray(details)?details:[]).flatMap((raw)=>{const detail=object(raw)
|
|
266
|
+
if(detail["@type"]!=="type.googleapis.com/google.rpc.QuotaFailure")
|
|
267
|
+
return []
|
|
268
|
+
return(Array.isArray(detail.violations)?detail.violations:[]).flatMap((rawViolation)=>{const violation=object(rawViolation)
|
|
269
|
+
const dimensions=object(violation.quotaDimensions)
|
|
270
|
+
const value=number(violation.quotaValue)
|
|
271
|
+
const id=typeof violation.quotaId==="string"?violation.quotaId:""
|
|
272
|
+
const metric=typeof violation.quotaMetric==="string"?violation.quotaMetric:""
|
|
273
|
+
const selected=dimensions.model??dimensions.base_model
|
|
274
|
+
if(selected!==apiModelID||value===undefined)
|
|
275
|
+
return []
|
|
276
|
+
const requestUnit=metric.includes("requests")?"requests":undefined
|
|
277
|
+
const unit=metric.includes("tokens")?"tokens":requestUnit
|
|
278
|
+
if(!unit)
|
|
279
|
+
return []
|
|
280
|
+
return [{id:`google-${id || metric}`,label:id||metric,modelID,limitID:id,kind:"configured_limit",unit,total:value
|
|
281
|
+
}]
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
function reset(value,now){if(!value||value.length>128)
|
|
286
|
+
return
|
|
287
|
+
if(value.includes("T")){const time=Date.parse(value)
|
|
288
|
+
return Number.isFinite(time)?time / 1000:undefined
|
|
289
|
+
}
|
|
290
|
+
const parts=[...value.matchAll(/(\d+(?:\.\d+)?)(ms|[dhms])/g)]
|
|
291
|
+
if(parts.length===0||parts.map((part)=>part[0]).join("")!==value)
|
|
292
|
+
return
|
|
293
|
+
const units={d:86400,h:3600,m:60,s:1,ms:0.001}
|
|
294
|
+
return now / 1000 + parts.reduce((seconds,part)=>seconds + Number(part[1])*units[part[2]],0)
|
|
295
|
+
}
|
|
296
|
+
function counterWindow(modelID,headers,now,prefix,suffix,label,unit,durationMinutes){const total=number(headers.get(`${prefix}limit-${suffix}`))
|
|
297
|
+
const remaining=number(headers.get(`${prefix}remaining-${suffix}`))
|
|
298
|
+
if(total===undefined&&remaining===undefined||total!==undefined&&total<0||remaining!==undefined&&remaining<0)
|
|
299
|
+
return []
|
|
300
|
+
const resetsAt=reset(headers.get(`${prefix}reset-${suffix}`),now)
|
|
301
|
+
return [{id:`${prefix}${suffix}`,label,modelID,unit,kind:"rate_limit",...total!==undefined&&{total},...remaining!==undefined&&{remaining},...total!==undefined&&remaining!==undefined&&{used:total-remaining},...durationMinutes!==undefined&&{durationMinutes},...resetsAt!==undefined&&{resetsAt}}]
|
|
302
|
+
}
|
|
303
|
+
function nativeHeaderWindows(providerID,modelID,headers,now=Date.now()){const windows=[]
|
|
304
|
+
const add=(prefix,suffix,label,unit,durationMinutes)=>{windows.push(...counterWindow(modelID,headers,now,prefix,suffix,label,unit,durationMinutes))
|
|
305
|
+
}
|
|
306
|
+
const genericFields=["openai","groq","azure"].includes(providerID)?["requests","tokens","project-tokens"]:[]
|
|
307
|
+
const groqPeriods={requests:1440,tokens:1,"project-tokens":1}
|
|
308
|
+
for(const field of genericFields){add("x-ratelimit-",field,field.replaceAll("-"," "),field==="requests"?"requests":"tokens",providerID==="groq"?groqPeriods[field]:undefined)
|
|
309
|
+
}
|
|
310
|
+
const periodDurations={minute:1,hour:60,day:1440}
|
|
311
|
+
const periods=providerID==="cerebras"?["minute","hour","day"]:[]
|
|
312
|
+
for(const period of periods){for(const field of ["requests","req","tokens"])
|
|
313
|
+
add("x-ratelimit-",`${field}-${period}`,`${field === "req" ? "requests" : field} / ${period}`,field==="tokens"?"tokens":"requests",periodDurations[period])
|
|
314
|
+
}
|
|
315
|
+
const anthropicFields=providerID==="anthropic"?["requests","tokens","input-tokens","output-tokens"]:[]
|
|
316
|
+
const priorityFields=providerID==="anthropic"?["input-tokens","output-tokens"]:[]
|
|
317
|
+
for(const field of anthropicFields)
|
|
318
|
+
add("anthropic-ratelimit-",field,field.replaceAll("-"," "),field==="requests"?"requests":"tokens",1)
|
|
319
|
+
for(const field of priorityFields)
|
|
320
|
+
add("anthropic-priority-",field,`Priority ${field.replaceAll("-", " ")}`,"tokens",1)
|
|
321
|
+
if(["fireworks-ai","fireworks"].includes(providerID)){for(const [field,label] of [["prompt","Total prompt throughput"],["cache-adjusted-prompt","Uncached prompt throughput"],["generated","Output throughput"]]){const total=number(headers.get(`x-ratelimit-limit-tokens-${field}`))
|
|
322
|
+
if(total!==undefined&&total>=0)
|
|
323
|
+
windows.push({id:`fireworks-${field}`,label,modelID,total,unit:"tokens/s",kind:"configured_limit"})
|
|
324
|
+
}}
|
|
325
|
+
return windows
|
|
326
|
+
}
|
|
327
|
+
function balanceSnapshot(model,providerName,values,fetchedAt=Date.now(),accountId){return{model,providerId:model.providerID,providerName,fetchedAt,windows:[],balances:values,...accountId&&{accountId},availability:values.length>0?"ready":"unavailable",...values.length===0&&{message:"The provider did not report an available balance; it is not assumed to be zero."
|
|
328
|
+
}}}
|
|
329
|
+
function deepSeekBalances(body){return(Array.isArray(body.balance_infos)?body.balance_infos:[]).flatMap((raw,index)=>{const row=object(raw)
|
|
330
|
+
const unit=typeof row.currency==="string"?row.currency:undefined
|
|
331
|
+
return [...balance(`available-${index}`,"Available balance",row.total_balance,unit),...balance(`granted-${index}`,"Granted balance",row.granted_balance,unit),...balance(`topup-${index}`,"Top-up balance",row.topped_up_balance,unit)]
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
function moonshotBalances(body,currency){const data=object(body.data)
|
|
335
|
+
return body.code===0&&body.status!==false?[...balance("available","Available balance",data.available_balance,currency),...balance("voucher","Voucher balance",data.voucher_balance,currency),...balance("cash","Cash balance",data.cash_balance,currency)]:[]
|
|
336
|
+
}
|
|
337
|
+
function siliconFlowBalances(body){const data=object(body.data)
|
|
338
|
+
return body.status===false?[]:[...balance("available","Available balance",data.totalBalance),...balance("charged","Charged balance",data.chargeBalance),...balance("granted","Granted balance",data.balance)]
|
|
339
|
+
}
|
|
340
|
+
function novitaBalances(body){return [["availableBalance","Available balance"],["cashBalance","Cash balance"],["creditLimit","Credit limit"],["pendingCharges","Pending charges"],["outstandingInvoices","Outstanding invoices"]].flatMap(([field,label])=>balance(field,label,scaledAmount(body[field],4),"USD"))
|
|
341
|
+
}
|
|
342
|
+
function miniMaxBalances(body){const status=number(object(body.base_resp).status_code)
|
|
343
|
+
if(status!==undefined&&status!==0)
|
|
344
|
+
return []
|
|
345
|
+
return [["available_amount","Available balance"],["cash_balance","Cash balance"],["voucher_balance","Voucher balance"],["credit_balance","Credit balance"],["owed_amount","Owed amount"]].flatMap(([field,label])=>balance(field,label,body[field]))
|
|
346
|
+
}
|
|
347
|
+
function poeBalances(body){return balance("points","Available plan and add-on points",body.current_point_balance,"points")
|
|
348
|
+
}
|
|
349
|
+
function nanoBalances(body){return [...balance("usd","Account balance",body.usd_balance,"USD"),...balance("nano","Balance in Nano",body.nano_balance,"NANO")]
|
|
350
|
+
}
|
|
351
|
+
function aiHubMixBalances(body){return body.success===true?balance("available","Available balance",scaledAmount(object(body.data).quota,6,2n),"USD"):[]
|
|
352
|
+
}
|
|
353
|
+
function openRouterBudget(body){const data=object(body.data)
|
|
354
|
+
const remaining=number(data.limit_remaining)
|
|
355
|
+
return data.limit===null?[{id:"key-budget",label:"Key spending budget",scope:"key",unlimited:true,unit:"USD"}]:balance("key-budget","Remaining key spending budget",remaining,"USD","key")
|
|
356
|
+
}
|
|
357
|
+
function miniMaxWindows(body,modelID,apiModelID){const responseStatus=number(object(body.base_resp).status_code)
|
|
358
|
+
if(responseStatus!==undefined&&responseStatus!==0)
|
|
359
|
+
return []
|
|
360
|
+
return(Array.isArray(body.model_remains)?body.model_remains:[]).flatMap((raw)=>{const row=object(raw)
|
|
361
|
+
if(row.model_name!==apiModelID)
|
|
362
|
+
return []
|
|
363
|
+
return ["interval","weekly"].flatMap((period)=>{const total=number(row[`current_${period}_total_count`])
|
|
364
|
+
const remainingPercent=number(row[`current_${period}_remaining_percent`])
|
|
365
|
+
const status=number(row[`current_${period}_status`])
|
|
366
|
+
if(total===undefined||total<=0||status===3)
|
|
367
|
+
return []
|
|
368
|
+
const reset=milliseconds(row[period==="weekly"?"weekly_end_time":"end_time"])
|
|
369
|
+
return [{id:`minimax-${period}`,label:period==="weekly"?"Weekly Token Plan":"Token Plan window",modelID,kind:remainingPercent===undefined?"configured_limit":"subscription",unit:remainingPercent===undefined?"usage units":"%",...remainingPercent===undefined?{total}:{used:100-remainingPercent,remaining:remainingPercent},...reset!==undefined&&{resetsAt:reset}}]
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
function nanoWindows(body,modelID){if(body.active!==true)
|
|
374
|
+
return []
|
|
375
|
+
const limits=object(body.limits)
|
|
376
|
+
return ["dailyInputTokens","weeklyInputTokens"].flatMap((period)=>{const row=object(body[period])
|
|
377
|
+
const total=number(limits[period])
|
|
378
|
+
const used=number(row.used)
|
|
379
|
+
const remaining=number(row.remaining)
|
|
380
|
+
if(row.degraded===true||total===undefined||used===undefined&&remaining===undefined)
|
|
381
|
+
return []
|
|
382
|
+
const resetsAt=milliseconds(row.resetAt)
|
|
383
|
+
return [{id:`nanogpt-${period}`,label:period==="dailyInputTokens"?"Daily subscription input tokens":"Weekly subscription input tokens",modelID,kind:"subscription",unit:"tokens",total,...used!==undefined&&{used},...remaining!==undefined&&{remaining},...resetsAt!==undefined&&{resetsAt}}]
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
function anthropicConfiguredWindows(body,modelID,apiModelID){return(Array.isArray(body.data)?body.data:[]).flatMap((raw)=>{const row=object(raw)
|
|
387
|
+
if(object(row.group).type!=="model_group"||!Array.isArray(row.models)||!row.models.includes(apiModelID))
|
|
388
|
+
return []
|
|
389
|
+
return(Array.isArray(row.limits)?row.limits:[]).flatMap((rawLimit)=>{const limit=object(rawLimit)
|
|
390
|
+
const value=number(limit.value)
|
|
391
|
+
const type=typeof limit.type==="string"?limit.type:""
|
|
392
|
+
const match=/^(requests|input_tokens|output_tokens|tokens)_per_(minute|day)$/.exec(type)
|
|
393
|
+
if(value===undefined||!match)
|
|
394
|
+
return []
|
|
395
|
+
return [{id:`anthropic-${row.id}-${type}`,label:type.replaceAll("_"," "),modelID,limitID:typeof row.id==="string"?row.id:undefined,kind:"configured_limit",unit:match[1].includes("tokens")?"tokens":"requests",durationMinutes:match[2]==="day"?1440:1,total:value
|
|
396
|
+
}]
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
async function readAnthropicQuota(input){const options={headers:{"anthropic-version":"2023-06-01"},...input.credential.type==="key"&&{apiKeyHeader:"x-api-key"}}
|
|
401
|
+
const windows=[]
|
|
402
|
+
let page
|
|
403
|
+
for(let index=0;index<10; index++){const url=new URL("https://api.anthropic.com/v1/organizations/rate_limits")
|
|
404
|
+
url.searchParams.set("model",input.apiModelID)
|
|
405
|
+
if(page)
|
|
406
|
+
url.searchParams.set("page",page)
|
|
407
|
+
const body=await readProviderJson(input.fetcher,url.href,input.token,input.signal,options)
|
|
408
|
+
windows.push(...anthropicConfiguredWindows(body,input.model.modelID,input.apiModelID))
|
|
409
|
+
page=typeof body.next_page==="string"&&body.next_page?body.next_page:undefined
|
|
410
|
+
if(!page)
|
|
411
|
+
break
|
|
412
|
+
}
|
|
413
|
+
return{...balanceSnapshot(input.model,input.provider.name,[]),windows,availability:windows.length>0?"ready":"unsupported",message:windows.length>0?"Configured rate limits; remaining capacity is not reported by this API.":"No explicit rate-limit group was reported for the selected model."
|
|
414
|
+
}}
|
|
415
|
+
var origins={deepseek:["https://api.deepseek.com"],moonshotai:["https://api.moonshot.ai"],"moonshotai-cn":["https://api.moonshot.cn"],siliconflow:["https://api.siliconflow.com"],"siliconflow-cn":["https://api.siliconflow.cn"],"novita-ai":["https://api.novita.ai"],poe:["https://api.poe.com"],aihubmix:["https://aihubmix.com","https://api.aihubmix.com"],minimax:["https://api.minimax.io","https://www.minimax.io"],"minimax-cn":["https://api.minimaxi.com","https://www.minimaxi.com"],"nano-gpt":["https://nano-gpt.com","https://api.nano-gpt.com"],anthropic:["https://api.anthropic.com"]
|
|
416
|
+
}
|
|
417
|
+
async function readNativeRest(input){const{canonical,token,signal,fetcher,model,provider,apiModelID}=input
|
|
418
|
+
const allowed=origins[canonical]
|
|
419
|
+
if(!allowed||!matchingOrigin(provider,allowed))
|
|
420
|
+
return
|
|
421
|
+
const read=(url,options)=>readProviderJson(fetcher,url,token,signal,options)
|
|
422
|
+
const make=(values,accountId)=>balanceSnapshot(model,provider.name,values,Date.now(),accountId)
|
|
423
|
+
switch(canonical){case "deepseek":{return make(deepSeekBalances(await read("https://api.deepseek.com/user/balance")))
|
|
424
|
+
}
|
|
425
|
+
case "moonshotai":{return make(moonshotBalances(await read("https://api.moonshot.ai/v1/users/me/balance"),"USD"))
|
|
426
|
+
}
|
|
427
|
+
case "moonshotai-cn":{return make(moonshotBalances(await read("https://api.moonshot.cn/v1/users/me/balance"),"CNY"))
|
|
428
|
+
}
|
|
429
|
+
case "siliconflow":case "siliconflow-cn":{const body=await read(`${allowed[0]}/v1/user/info`)
|
|
430
|
+
const id=object(body.data).id
|
|
431
|
+
return make(siliconFlowBalances(body),typeof id==="string"?id:undefined)
|
|
432
|
+
}
|
|
433
|
+
case "novita-ai":{return make(novitaBalances(await read("https://api.novita.ai/openapi/v1/billing/balance/detail")))
|
|
434
|
+
}
|
|
435
|
+
case "poe":{return make(poeBalances(await read("https://api.poe.com/usage/current_balance")))
|
|
436
|
+
}
|
|
437
|
+
case "aihubmix":{return make(aiHubMixBalances(await read("https://aihubmix.com/api/user/self")))
|
|
438
|
+
}
|
|
439
|
+
case "minimax":case "minimax-cn":{const base=allowed[0]
|
|
440
|
+
if(input.credential.type==="key"&&token.startsWith("sk-api-"))
|
|
441
|
+
return make(miniMaxBalances(await read(`${base}/account/query_balance`)))
|
|
442
|
+
const body=await read(`${base}/v1/token_plan/remains`)
|
|
443
|
+
const windows=miniMaxWindows(body,model.modelID,apiModelID)
|
|
444
|
+
return{...make([]),windows,availability:windows.length>0?"ready":"unavailable",message:windows.length>0?undefined:"No finite, model-associated Token Plan allowance was reported. Missing or ambiguous counts are not treated as remaining quota."
|
|
445
|
+
}}
|
|
446
|
+
case "nano-gpt":{const configured=nativeBaseURL(provider)
|
|
447
|
+
const base=configured?new URL(configured).origin:allowed[0]
|
|
448
|
+
const balances=nanoBalances(await read(`${base}/api/check-balance`,{method:"POST",apiKeyHeader:"x-api-key"}))
|
|
449
|
+
let subscription
|
|
450
|
+
try{subscription=await Promise.all([read(`${base}/api/subscription/v1/usage`),read(`${base}/api/subscription/v1/models`)])
|
|
451
|
+
} catch(error){if(signal.aborted||balances.length===0)
|
|
452
|
+
throw error
|
|
453
|
+
return{...make(balances),message:"Account balance is available; subscription counters could not be read."}}
|
|
454
|
+
const [usage,models]=subscription
|
|
455
|
+
const covered=Array.isArray(models.data)&&models.data.some((entry)=>object(entry).id===apiModelID)
|
|
456
|
+
const windows=covered?nanoWindows(usage,model.modelID):[]
|
|
457
|
+
return{...make(balances),windows,...windows.length>0&&{availability:"ready"},...windows.length===0&&{message:"Subscription windows are shown only for provider-listed subscription models with non-degraded counters."
|
|
458
|
+
}}}
|
|
459
|
+
case "anthropic":{return readAnthropicQuota(input)
|
|
460
|
+
}}
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
function openRouterFreeWindow(body,modelID,apiModelID){if(!apiModelID.endsWith(":free"))
|
|
464
|
+
return []
|
|
465
|
+
const data=object(object(body.data).free_model_daily_requests)
|
|
466
|
+
const total=number(data.limit)
|
|
467
|
+
const remaining=number(data.remaining)
|
|
468
|
+
const used=number(data.used)
|
|
469
|
+
return total!==undefined&&(used!==undefined||remaining!==undefined)?[{id:"openrouter-free-daily",label:"Free-model daily requests",modelID,unit:"requests",durationMinutes:1440,kind:"rate_limit",total,...used!==undefined&&{used},...remaining!==undefined&&{remaining}}]:[]
|
|
470
|
+
}
|
|
471
|
+
function managementCredits(body){const data=object(body.data)
|
|
472
|
+
const total=number(data.total_credits)
|
|
473
|
+
const used=number(data.total_usage)
|
|
474
|
+
return total!==undefined&&used!==undefined?balance("account-credits","Remaining account credits",total-used,"USD"):[]
|
|
475
|
+
}
|
|
476
|
+
async function readOpenRouterQuota(input){if(!matchingOrigin(input.provider,["https://openrouter.ai"]))
|
|
477
|
+
return
|
|
478
|
+
const body=await readProviderJson(input.fetcher,"https://openrouter.ai/api/v1/key",input.token,input.signal)
|
|
479
|
+
const data=object(body.data)
|
|
480
|
+
const balances=openRouterBudget(body)
|
|
481
|
+
if(data.is_management_key===true)
|
|
482
|
+
balances.push(...managementCredits(await readProviderJson(input.fetcher,"https://openrouter.ai/api/v1/credits",input.token,input.signal)))
|
|
483
|
+
const windows=openRouterFreeWindow(body,input.model.modelID,input.apiModelID)
|
|
484
|
+
const id=typeof data.organization_id==="string"?data.organization_id:undefined
|
|
485
|
+
return{...balanceSnapshot(input.model,input.provider.name,balances,Date.now(),id),windows,...windows.length>0&&{availability:"ready"},message:data.is_management_key===true?undefined:"Key budget, not account balance. Reading account credits requires a management key."
|
|
486
|
+
}}
|
|
487
|
+
async function nativeQuotaContext(host,model,signal,fetcher){const [provider,catalog]=await Promise.all([host.provider.get({providerID:model.providerID}),host.model.list()])
|
|
488
|
+
const selected=catalog.data.find((entry)=>entry.providerID===model.providerID&&entry.id===model.modelID&&entry.enabled)
|
|
489
|
+
if(!selected||!provider.data.integrationID||provider.data.activation==="disabled")
|
|
490
|
+
return
|
|
491
|
+
const connection=await host.integration.connection.active(provider.data.integrationID)
|
|
492
|
+
if(!connection)
|
|
493
|
+
return
|
|
494
|
+
const credential=await host.integration.connection.resolve(connection)
|
|
495
|
+
const token=credential?.type==="oauth"?credential.access:credential?.key
|
|
496
|
+
if(!credential||!token)
|
|
497
|
+
return
|
|
498
|
+
signal.throwIfAborted()
|
|
499
|
+
return{model,apiModelID:selected.modelID,provider:provider.data,canonical:providerQuotaCapability(provider.data.id,provider.data.canonical).id,connection,connectionID:connectionKey(connection),credential,token,signal,fetcher
|
|
500
|
+
}}
|
|
501
|
+
async function sameNativeConnection(host,input){const current=input.provider.integrationID&&await host.integration.connection.active(input.provider.integrationID)
|
|
502
|
+
if(!current||connectionKey(current)!==input.connectionID)
|
|
503
|
+
return false
|
|
504
|
+
const credential=await host.integration.connection.resolve(current)
|
|
505
|
+
const token=credential?.type==="oauth"?credential.access:credential?.key
|
|
506
|
+
return token===input.token
|
|
507
|
+
}
|
|
508
|
+
function requestUsesCredential(request,input){const url=new URL(request.url)
|
|
509
|
+
const origins={openai:"https://api.openai.com",anthropic:"https://api.anthropic.com",groq:"https://api.groq.com",cerebras:"https://api.cerebras.ai","fireworks-ai":"https://api.fireworks.ai",google:"https://generativelanguage.googleapis.com"
|
|
510
|
+
}
|
|
511
|
+
const cloudOrigin=url.protocol==="https:"&&(input.canonical==="azure"&&url.hostname.endsWith(".openai.azure.com")||input.canonical==="google-vertex"&&url.hostname.endsWith(".aiplatform.googleapis.com"))
|
|
512
|
+
if(!cloudOrigin&&url.origin!==origins[input.canonical])
|
|
513
|
+
return false
|
|
514
|
+
const authorization=request.headers.get("authorization")
|
|
515
|
+
if(authorization===`Bearer ${input.token}`||authorization===`bearer ${input.token}`)
|
|
516
|
+
return true
|
|
517
|
+
if(["x-api-key","api-key","x-goog-api-key"].some((header)=>request.headers.get(header)===input.token))
|
|
518
|
+
return true
|
|
519
|
+
return url.hostname==="generativelanguage.googleapis.com"&&url.searchParams.get("key")===input.token
|
|
520
|
+
}
|
|
521
|
+
function key2(input){return JSON.stringify([input.model.providerID,input.model.modelID,input.model.variant,input.connectionID,createHash("sha256").update(input.token).digest("hex")])
|
|
522
|
+
}
|
|
523
|
+
function modelForEvent(event){return{providerID:event.model.providerID,modelID:event.model.id,sessionID:event.sessionID}}
|
|
524
|
+
function rejectedRead(model,error,input){const status=error instanceof QuotaReadError?error.status:undefined
|
|
525
|
+
const states={429:"rate_limited",401:"unauthenticated",403:"permission_required",404:"unsupported"
|
|
526
|
+
}
|
|
527
|
+
return{model,providerId:model.providerID,providerName:input?.provider.name??providerQuotaCapability(model.providerID).name,fetchedAt:Date.now(),windows:[],...input&&{connectionID:input.connectionID},availability:status===undefined?"unavailable":states[status]??"unavailable",message:status===403?"The active credential does not have the provider quota/billing permission required by this API.":"The documented provider quota source is unavailable. No credentials or raw provider error details are exposed."
|
|
528
|
+
}}
|
|
529
|
+
function createNativeQuotaService(host,lifecycle,fetcher=fetch,copilot=createCopilotQuotaReader()){const observed=new Map
|
|
530
|
+
const cached=new Map
|
|
531
|
+
const captured=new WeakMap
|
|
532
|
+
let generation=0
|
|
533
|
+
const base=(model)=>({model,providerId:model.providerID,providerName:providerQuotaCapability(model.providerID).name,fetchedAt:Date.now(),windows:[],availability:"unavailable"
|
|
534
|
+
})
|
|
535
|
+
async function rest(input){if(input.canonical==="openrouter")
|
|
536
|
+
return readOpenRouterQuota(input)
|
|
537
|
+
if(input.canonical==="fireworks-ai")
|
|
538
|
+
return readFireworksQuota(input)
|
|
539
|
+
if(input.canonical==="github-copilot")
|
|
540
|
+
return copilot.read(input)
|
|
541
|
+
return readNativeRest(input)
|
|
542
|
+
}
|
|
543
|
+
async function currentResult(input,force){const identity=key2(input)
|
|
544
|
+
const headers=observed.get(identity)
|
|
545
|
+
const existing=cached.get(identity)
|
|
546
|
+
let result=!force&&existing&&Date.now()-existing.fetchedAt<30000?existing:undefined
|
|
547
|
+
if(headers&&input.canonical==="anthropic")
|
|
548
|
+
result=headers
|
|
549
|
+
else if(!result){try{result=await rest(input)
|
|
550
|
+
} catch(error){if(!headers)
|
|
551
|
+
throw error
|
|
552
|
+
result=headers
|
|
553
|
+
}}
|
|
554
|
+
return{identity,headers,result:result??headers}}
|
|
555
|
+
return{invalidate(){generation++
|
|
556
|
+
cached.clear()
|
|
557
|
+
observed.clear()
|
|
558
|
+
},async request(event){if(lifecycle.aborted||event.kind!=="primary"||!/(?:\/chat\/completions|\/messages|\/responses|:generateContent|:streamGenerateContent)$/.test(new URL(event.request.url).pathname))
|
|
559
|
+
return
|
|
560
|
+
const epoch=generation
|
|
561
|
+
const observation=AbortSignal.any([lifecycle,AbortSignal.timeout(1000)])
|
|
562
|
+
try{const input=await withinQuotaSignal(observation,()=>nativeQuotaContext(host,modelForEvent(event),observation,fetcher))
|
|
563
|
+
if(input&&epoch===generation&&requestUsesCredential(event.request,input)&&await withinQuotaSignal(observation,()=>sameNativeConnection(host,input)))
|
|
564
|
+
captured.set(event.request,{key:key2(input),generation:epoch})
|
|
565
|
+
} catch{}},async response(event){const request=captured.get(event.request)
|
|
566
|
+
if(!request||request.generation!==generation||lifecycle.aborted)
|
|
567
|
+
return false
|
|
568
|
+
const observation=AbortSignal.any([lifecycle,AbortSignal.timeout(1000)])
|
|
569
|
+
try{const input=await withinQuotaSignal(observation,()=>nativeQuotaContext(host,modelForEvent(event),observation,fetcher))
|
|
570
|
+
if(!input||key2(input)!==request.key||!await withinQuotaSignal(observation,()=>sameNativeConnection(host,input)))
|
|
571
|
+
return false
|
|
572
|
+
let windows=nativeHeaderWindows(input.canonical,input.model.modelID,event.response.headers)
|
|
573
|
+
if(windows.length===0&&event.response.status===429&&["google","google-vertex"].includes(input.canonical)&&event.response.headers.get("content-type")?.includes("json"))
|
|
574
|
+
windows=googleQuotaFailureWindows(await readQuotaResponseBody(event.response.clone(),observation),input.model.modelID,input.apiModelID)
|
|
575
|
+
if(windows.length===0||request.generation!==generation)
|
|
576
|
+
return false
|
|
577
|
+
observed.set(request.key,{...base(input.model),providerName:input.provider.name,connectionID:input.connectionID,availability:"ready",windows,message:"Provider-native API limits observed from this model response; no extra inference request was made."
|
|
578
|
+
})
|
|
579
|
+
return true
|
|
580
|
+
} catch{return false
|
|
581
|
+
}},async read(target,signal){const model={providerID:target.providerID,modelID:target.modelID,sessionID:target.sessionID,...target.variant&&{variant:target.variant}}
|
|
582
|
+
const epoch=generation
|
|
583
|
+
const combined=AbortSignal.any([lifecycle,signal,AbortSignal.timeout(15000)])
|
|
584
|
+
let input
|
|
585
|
+
try{input=await withinQuotaSignal(combined,()=>nativeQuotaContext(host,model,combined,fetcher))
|
|
586
|
+
if(!input)
|
|
587
|
+
return{...base(model),availability:"unauthenticated",message:"No eligible active provider connection or selected model was reported by OpenCode."
|
|
588
|
+
}
|
|
589
|
+
if(target.connectionID&&target.connectionID!==input.connectionID)
|
|
590
|
+
return{...base(model),availability:"unauthenticated",message:"The OpenCode connection changed; the old account is not queried."
|
|
591
|
+
}
|
|
592
|
+
const current=input
|
|
593
|
+
const{identity,headers,result}=await withinQuotaSignal(combined,()=>currentResult(current,target.force))
|
|
594
|
+
combined.throwIfAborted()
|
|
595
|
+
if(epoch!==generation||!await withinQuotaSignal(combined,()=>sameNativeConnection(host,current)))
|
|
596
|
+
return{...base(model),availability:"unauthenticated",message:"The provider account changed during the quota read."
|
|
597
|
+
}
|
|
598
|
+
if(!result){const capability=providerQuotaCapability(input.canonical)
|
|
599
|
+
return{...base(model),providerName:input.provider.name,connectionID:input.connectionID,availability:capability.sources.length>0?"waiting":"unsupported",message:capability.reason
|
|
600
|
+
}}
|
|
601
|
+
const freshHeaders=headers&&Date.now()-headers.fetchedAt<=120000
|
|
602
|
+
const merged={...result,model,connectionID:input.connectionID,windows:[...result.windows,...freshHeaders&&headers!==result?headers.windows:[]],...headers===result&&Date.now()-result.fetchedAt>120000&&{availability:"stale"}}
|
|
603
|
+
if(headers!==result)
|
|
604
|
+
cached.set(identity,{...result,model,connectionID:input.connectionID})
|
|
605
|
+
return merged
|
|
606
|
+
} catch(error){if(signal.aborted||lifecycle.aborted)
|
|
607
|
+
throw error
|
|
608
|
+
return rejectedRead(model,error,input)
|
|
609
|
+
}},async dispose(){cached.clear()
|
|
610
|
+
observed.clear()
|
|
611
|
+
await copilot.dispose()
|
|
612
|
+
}}}
|
|
613
|
+
async function setupProviderQuotaServer(host){const lifecycle=new AbortController
|
|
614
|
+
const service=createNativeQuotaService(host,lifecycle.signal)
|
|
615
|
+
const rpc=await host.rpc.register(NavigatorQuotaRpc,{read:async (input,context)=>normalizeProviderQuota(await service.read(input,context.signal)),capabilities:async ()=>{const providers=await host.provider.list()
|
|
616
|
+
return providerQuotaCatalog(providers.data)
|
|
617
|
+
}})
|
|
618
|
+
const request=await host.session.hook("http.request",(event)=>service.request(event))
|
|
619
|
+
const response=await host.session.hook("http.response",async (event)=>{if(await service.response(event))
|
|
620
|
+
await rpc.events.emit("updated",{providerID:event.model.providerID,modelID:event.model.id}).catch(()=>{})
|
|
621
|
+
});
|
|
622
|
+
(async ()=>{const events=host.event.subscribe({signal:lifecycle.signal})
|
|
623
|
+
for await (const event of events){if(["credential.updated","credential.switched","integration.updated","provider.updated"].includes(event.type))
|
|
624
|
+
service.invalidate()
|
|
625
|
+
}})().catch(()=>{})
|
|
626
|
+
return async ()=>{lifecycle.abort()
|
|
627
|
+
await request.dispose()
|
|
628
|
+
await response.dispose()
|
|
629
|
+
await rpc.dispose()
|
|
630
|
+
await service.dispose()
|
|
631
|
+
}}
|
|
46
632
|
var input={type:"object",properties:{todos:NavigatorTodoRpc.methods.replace.input.properties.todos
|
|
47
633
|
},required:["todos"],additionalProperties:false
|
|
48
634
|
}
|
|
@@ -64,7 +650,9 @@ const context=await host.session.hook("context",async (event)=>{const state=awai
|
|
|
64
650
|
if(state.guidance)
|
|
65
651
|
event.system.push({type:"text",text:todoGuidance(state)})
|
|
66
652
|
})
|
|
67
|
-
|
|
653
|
+
const quotaCleanup=typeof host.integration?.connection?.active==="function"&&typeof host.provider?.get==="function"?await setupProviderQuotaServer(host):undefined
|
|
654
|
+
return async ()=>{await quotaCleanup?.()
|
|
655
|
+
await context.dispose()
|
|
68
656
|
await tools.dispose()
|
|
69
657
|
await rpc.dispose()
|
|
70
658
|
}}})
|