Repository navigation
Provider profiles: expose a serviceTier field to pin service_tier in request bodies (OpenRouter / OpenAI-compatible routes) #9787
GnaneshKunal
started this conversation in
Ideas
Replies: 2 comments
|
Single combined patch — host + client in one commit (
What it covers:
Verification: typecheck 5/5 · build 9/9 · oxlint clean · ui-settings-models 351/351 · llm-pi-ai 426/426 · the rendered select was visually confirmed on a live build (preselected from a configured route). |
0 replies
|
Superseded — everything above is consolidated into the single combined patch (host + client, one commit). The gist linked in the first comment holds both the |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Problem
OpenRouter (and OpenAI's own Responses endpoints) price and schedule requests by service tier:
flexhalves token costs in exchange for a slower pool,priority/fast/ultrafastpay multipliers for lower latency. Today, a DeepSeek Harness deployment routed through pi-ai (@deepseek-ai/dsh-llm-pi-ai) can only influence the tier indirectly, if at all::nitro,:floor), not theservice_tierrequest parameter, and the variants' semantics do not line up with the tiers.headersbut no request-body fields, soservice_tieris unreachable from configuration.Because request bodies are replay/cache-sensitive state, this has to be a configuration-level field rather than a per-call option. A tier affects pricing (flex halves, priority/fast double) and latency class for the whole route; it must be pinned once per provider profile and replayed identically on every request, the same way
baseURL,headers, andretryPolicyalready are. A per-call override would make the same conversation produce differently-priced request bodies, and a per-model suffix would leak provider-specific spelling into route-agnostic configuration.Evidence and parity
pi-ai implements it. The installed
@earendil-works/pi-ai(1.1.0) implements service tiers for itsopenai-responsesAPI: the options type acceptsserviceTier?: ResponseCreateParamsStreaming["service_tier"], writesparams.service_tier = options.serviceTierinto the request body, applies the flex/priority/fast cost multipliers to usage pricing, and resolves the served tier from the response ([pi-ai dist:api/openai-responses.js,api/openai-responses-shared.js]). The accepted value union is OpenAI'sServiceTier—auto,default,flex,scale,priority,fast,ultrafast(plusnull).opencode (same pi-ai lineage) already ships this as a config overlay. opencode exposes provider/model/variant-scoped
bodyoverlays merged into request bodies and documents"body": { "service_tier": "flex" }as the canonical example:So there is upstream precedent for treating the tier as configuration, not a call-site concern.
Proposed API
Add an optional, schema-validated
serviceTiertoPiAiProviderProfile(theprovidersdict in@deepseek-ai/dsh-llm-pi-aiconfig). Omitted means never sendservice_tier— the provider's own tier selection stays untouched (its served tier competes with a pinned one per the provider's own pricing rules).And the schema entry, in the same file (it renders as a form control in the Models settings automatically):
Example configuration:
Implementation note (verified against the installed pi-ai 1.1.0)
The obvious passthrough — copying
serviceTierinto theSimpleStreamOptionsobject the adapter hands pi-ai — is a silent no-op. pi-ai'sopenai-responsesstreamSimplemapping builds its request frombuildBaseOptions(...), which does not copy aserviceTierkey (only the fullstream/OpenAIResponsesOptionspath honors it, and the adapter callsstreamSimple). The wire seam pi-ai does honor on both OpenAI-compatible protocols issamplingParams: its entries are merged into the request body after the named fields ("Only applied by OpenAI-compatible adapters (completions, responses, Azure responses); other APIs ignore it" — pi-ai's own JSDoc). So the adapter maps the configured tier as:This keeps the field config-level, honors omission exactly, works for chat-completions and Responses routes, and degrades safely on every other protocol (anthropic-messages etc. ignore it).
Compat notes
service_tierkey is written to any request body, and resolution keeps the field absent (distinguishable from any sent value).samplingParams, chat-completions routes getservice_tiertoo (it is a valid OpenAI chat-completions parameter). The limitation that remains is on pi-ai's side: noapplyServiceTierPricing-style cost multiplier is applied on the completions path, so usage costs on completions routes are not tier-adjusted; the wire field is still sent correctly. A dedicated pi-aistreamSimpleoption would be the cleaner long-term home; thesamplingParamsmapping is pinned to the installed 1.1.0 behavior and should be re-visited on pi-ai upgrades (this repo already carries a pi-ai patch and a drift-gate convention for exactly this).Test/build evidence
pnpm run typecheck— 5/5 gates pass.pnpm run build— 9/9 gates pass (host compiler, Typert, Host bundle, Desktop bundle, client compiler, Client bundle, Web build, aggregate checks).vitest run packages/llm/llm-pi-ai/— 18 files, 426 tests, all pass, including:turbo) at the write path, keeps an omitted tier absent afterresolveProfiles;profileOptionsmapping: configured tier appears assamplingParams: { service_tier }on the options handed to pi-ai, absent entirely when unset;openai-responsesmock-server assertions: pinnedflexproducesservice_tier: 'flex'in the request body with a cleanstopfinish; unset produces noservice_tierkey at all.lib/index.js):ConfigacceptsserviceTier: 'flex'and materializes it, rejectsturbo, andPiAiAdapterresolves the profile carrying it.dsh web --no-open --port 30814 --host 127.0.0.1from this checkout serves HTTP 200 (via the boot-issued trust-token link; unauthenticated requests get the app's expected 401 fence), then shut down cleanly.Update — client UI select added (Oct 15)
Browser verification surfaced a gap the first patch missed: the Settings → Models provider card is hand-written per adapter family (
packages/client/ui-settings-models/src/client/ProviderEditor.tsxcurates its fields; "the card names only the fields it can see"), so a new schema field alone is stored but never rendered. Follow-up commitc90ee87ed0adds the missing client half:<select>on the pi-ai family's Customized settings fold (all pi-ai cards — a catalog route's profile names no route-levelapi, so gating on protocol would hide the control exactly on OpenRouter-style routes; the hint discloses that non-OpenAI protocols ignore the field)unionChoices()instore.ts) so client and host can't drift; Not set emits anunsetpath op, a pick emits asetopVerification: typecheck 5/5 · build 9/9 · oxlint clean · ui-settings-models 351/351 · llm-pi-ai 426/426 · visually confirmed on a live build (select renders preselected from a configured route).
Latest patch: refreshed single-commit patch at https://gist.github.com/GnaneshKunal/f80e41d36008a83967d8293ef7ad0db9 (holds both the git-am patch and the full diff) (see also the follow-up comment below).
All reactions