Repository navigation
Conversation
Add local routes for OpenRouter embeddings and asynchronous video jobs:
POST /v1/embeddings, POST /v1/videos, GET /v1/videos/{id},
GET /v1/videos/{id}/content, DELETE /v1/videos/{id}, and
GET /v1/{embeddings,videos}/models.
Models must have an explicit tier in the issuer's model policy and appear
in OpenRouter's catalog for that endpoint; there is no untiered fallback.
The live policy enables none, so the routes stay inert until the operator
enables models.
Embeddings share chat's lease, reuse and no-retry rules. A video job takes a
fresh lease and owns the wallet's single request slot until the job fails,
its outputs are downloaded, it is deleted, or the key's usable lifetime
ends; settlement then retires the key as usual. Job state is in memory
only, holds no prompt, and the provider job ID and key never leave the
client. callback_url, previous_job_id and identity fields are dropped, and
provider-supplied content URLs are never followed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZTNAFcTA7BQNUu6U83Gdc
Describe the new routes, that they need operator-enabled models, how a video job holds the wallet's lease, and the provider's retention of generated video. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZTNAFcTA7BQNUu6U83Gdc
Two submissions that both arrived while the request slot was busy passed the active-job check, and the second then started its own paid job as soon as the first job released the lease. A submission now claims a flag under videoMu together with that check, so a concurrent one gets 409 video_job_active. The docs also list cancelled and upstream-expired jobs among the release conditions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZTNAFcTA7BQNUu6U83Gdc
invalid_model on /v1/embeddings and /v1/videos now names /v1/embeddings/models and /v1/videos/models instead of /v1/models, and the 404 fallback lists the new routes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZTNAFcTA7BQNUu6U83Gdc
Legatia
added a commit
to pt-evm/zkapi
that referenced
this pull request
Oct 8, 2026
Legatia
marked this pull request as ready for review
October 8, 2026 05:11
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client side of #20.
Problem
zkapi-clientd exposes only chat completions and the model list. OpenRouter
also offers embeddings and an asynchronous video API, and clients of the
daemon need both. Video differs from chat: a job runs for minutes, and
polling and downloading need the key that submitted it.
Change
New local routes, mirroring OpenRouter's shapes:
POST /v1/embeddings,GET /v1/embeddings/modelsPOST /v1/videos(202, local job ID),GET /v1/videos/{id},GET /v1/videos/{id}/content?index=N,DELETE /v1/videos/{id},GET /v1/videos/modelsModel gate: a model is usable on an endpoint only if the issuer's policy
gives it an explicit reviewed tier and OpenRouter's public catalog for that
endpoint lists it. There is no untiered fallback and no
:onlinenormalization. The current policy enables none, so every request returns
400 model_not_enabledand the model lists are empty.Structure:
internal/zkapi/client.go:Completebecomesforward(endpoint), sharedwith embeddings.
waitForLeasetakes the endpoint for its budget lookup;freshendpoints never reuse or cache a key.internal/zkapi/media.go: endpoint table, endpoint-scoped budget,MediaModels,Embeddings.internal/zkapi/videos.go: video job lifecycle.internal/server/media.go: optionalMediaBackendinterface, routing,field allowlists.
server.gosplits chat forwarding intopostandrelay,which the new routes reuse; chat behaviour is unchanged.
cmd/zkapi-clientd/media.go:zkInferencemedia methods and safe errormapping.
Requests keep chat's rules: keyless loopback or local API key, browser
origins rejected, 16 MiB bodies, field allowlists (drops
user,session_id,trace, routing preferences; video also dropscallback_urland
previous_job_id, keepsprovider.options), the402 funding_requiredand wallet error mapping, no retries, and fixed log route labels
(
/v1/videos/{id}) without IDs.Lease handling
cached key may serve both, as documented for chat.
first). The job then owns the wallet's single request slot until it fails,
is cancelled or expires upstream, all outputs are downloaded completely,
the finished job is deleted, or the key's usable lifetime ends (a timer).
the key through the existing
/wallet/settlepath.submission made while another job is active or being submitted returns
409 video_job_active. A submission claims that state under the same lockas the active-job check, so two concurrent submissions start one job.
and tracked, so a created job is not orphaned. Submission is never retried.
unsigned_urls. Content isfetched only from
{inference base}/videos/{job}/contenton theconfigured origin.
no prompt or video, and the provider job ID and key never leave the
client. The Go side persists no lease state today; companion recovery
(Recover interrupted OA leases in server and CLI #12) still covers a restart mid-job.
Client-only vs operator-dependent
Client-only (this PR): the routes, model gate, lease hold and release,
error mapping, docs.
Operator or server, tracked in the proposal issue:
kindfield;price;
callback_urlon the org account.Embeddings need only policy entries.
Tests
internal/zkapi/media_test.go(fake companion and OpenRouter,synctest):model_not_enabledfor a chat model, unpriced, disabled and uncataloguedmodels, with no lease and no provider call;
402before any provider call;blocked meanwhile, no settlement until the download completes, then
settlement and a fresh chat key;
410;409;internal/server/media_test.go:405and404;Retry-After;404;cmd/zkapi-clientd/media_test.go,media_e2e_test.go: error mapping(
invalid_modelnames the media model lists), plusan end-to-end run through the real local API, mapping and client (funding
required, model gate, full video lifecycle, logs without prompts, IDs or
keys).
Verification
In
zkapi-clientd, the same commands as CI:The new tests passed 10 consecutive
-raceruns. A trial merge with #19(
clientd-scriptable-funding) has no conflicts, and build and tests pass onthe merged tree. No live provider or wallet calls were made.
Out of scope
callback_url);previous_job_id);GET /v1/models;A generic
/v1/*passthrough was considered and not chosen: each endpointneeds its own field allowlist, catalog scope and response type, and async
endpoints need lease ownership. The synchronous path is shared instead
(
forward(endpoint)), so another synchronous endpoint is a table entry plusan allowlist.
Follow-up after #19 merges:
zkInference.Completeshould usewalletError,and
walletErrorshould take #19's updatedfunding_required/withdrawal_pendingmessages (until then the media routes still say "Runzkapi-clientd config to add funding"). This PR keeps its own copy so it stays
independent of #19.
Merge notes: main + #19 + #21 merge cleanly in either order. With #16 (Tor
timeouts) there is one trivial conflict in
model_budget.go: this PR movespublicModelIDs' timeout intocatalogIDs, so #16's 60 s applies there.🤖 Generated with Claude Code
https://claude.ai/code/session_01JZTNAFcTA7BQNUu6U83Gdc