openai-api endpoint from the conductor¶
Issue: dashecorp/rig-agent-runtime#674 — part 3 of rig-conductor#2008 (volume-first wake).
Problem¶
Rig-E's brain is a vLLM pod on RunPod. Its URL embeds the pod id
(https://<podId>-8000.proxy.runpod.net/v1), and every migration or volume-first wake onto a
new pod changes the id — which, with a literal llm.baseUrl in the HelmRelease, costs a
gitops PR each time (rig-gitops#729/#730). The conductor already knows the truth:
GET /api/pods/{name} → { podId, proxyUrl, status, isRunning, lookup, … }.
Decision¶
llm.baseUrlFrom = { conductorPod, conductorUrl? } makes the provider ask the conductor.
llm.baseUrl alone keeps the literal path byte-for-byte as before (#668/#669).
| Rule | Behaviour |
|---|---|
| Source | baseUrlFrom wins over baseUrl (one warning). conductorUrl overrides CONDUCTOR_BASE_URL; neither set → config error at boot. Shape is validated at character load. |
| Ready | isRunning === true and proxyUrl is a bare https origin on an allowed host → baseUrl = origin + "/v1", endpoint …/chat/completions. isRunning: false (any status) → not ready. |
| proxyUrl trust | The conductor is unauthenticated in-cluster and its answer decides where the bearer key is POSTed (SSRF-by-conductor-response). A proxyUrl with embedded credentials, a path, a query, a fragment, a non-https scheme, or a host outside baseUrlFrom.allowedHostSuffixes (default [".proxy.runpod.net"], matched as host === s \|\| host.endsWith("." + s); empty list = default) is refused: not ready, logged as proxyUrl rejected: <reason>, never used. The endpoint is always built from url.origin. The own-GPU move is a config change to allowedHostSuffixes. |
| Conductor unreadable | Transport error or lookup: UNREADABLE → keep the last-known endpoint if there is one, else retry the GET 3× with 5 s between (in-turn), then not ready. 404 (unknown pod name) → config error, not retried. |
| Not ready | AgentProviderError('openai-api', …, { fallbackEligible: false, retryable: true, providerNotReady: { pod, status, lookup, podId } }). processWithProviders (agent.js) copies providerNotReady and httpStatus onto its final re-throw, so the fields reach the chat.js catch. chat.js reads it in its wake-on-chat path and answers 200 with a waking reply — see Wake-on-chat. |
| Re-resolve | A completion that fails with a network error or HTTP 404/502/503/504 invalidates the cache, re-resolves ONCE per chat turn and retries the completion once, then throws. 400/401/429/500 never re-resolve. Completion errors now carry httpStatus. |
| Cache | Single-flight (concurrent turns share one GET); invalidated only by the re-resolve path; no background polling. |
| Boot | Resolves once so openai-api endpoint: https://…/chat/completions (pod <id> status RUNNING) is logged when ready; when not ready it logs openai-api endpoint: not ready (pod qwen status EXITED lookup FOUND) — will resolve on first turn and the agent still boots. Config errors still throw at boot. |
| Logging | Every (re)resolution logs pod id + status. The API key is never logged; the in-cluster conductor endpoint is unauthenticated, so no bearer is sent to it. |
Shape¶
Pure policy + adapter + wiring, matching the repo's split:
src/agent/providers/endpoint-resolver.js—decideEndpointSource(llm, env),endpointFromPodStatus(json),shouldRefreshEndpoint(error)(pure);createConductorPodClient({ conductorUrl })(adapter, 5 sAbortSignal.timeout);createEndpointResolver({ source, podClient })→current()/invalidate().src/agent/providers/openai-api.js— builds the resolver at creation, awaitscurrent()per turn, applies the one-shot re-resolve, exposesendpointResolution(boot promise) for tests.src/agent/provider-error.js—httpStatusandproviderNotReadyoptions.src/agent.js—processWithProviderspropagatesproviderNotReady/httpStatuson the final re-throw.src/character.js—llm.baseUrlFromdefaults tonull; shape-checked via the pure function.
Character example¶
"llm": {
"provider": "openai-api",
"model": "Qwen/Qwen2.5-32B-Instruct-AWQ",
"baseUrlFrom": {
"conductorPod": "qwen",
"conductorUrl": "http://rig-conductor-api.rig-conductor.svc.cluster.local:8080"
}
}
Rig-E has no CONDUCTOR_BASE_URL env today, so conductorUrl is set explicitly in the
HelmRelease (rig-gitops side of #674). No chart change is needed: character is opaque.
Tests¶
endpoint-resolver.test.js (policy incl. proxyUrl rejection + allow-list, adapter, cache/single-flight/retry with injected sleep),
agent.test.js (providerNotReady + httpStatus survive the final re-throw),
openai-api.test.js (URL-routed fake fetch: resolve, re-resolve on 404 → new pod id, no
re-resolve on 400, not-ready hook, literal path unchanged), and
openai-api.integration.test.js (an in-process http.createServer mock conductor: ready
boot, EXITED boot, pod id change between two turns).