Skip to content

Add scoped schema 2.4 provider discovery catalog - #1041

Open
PierreLeGuen wants to merge 1 commit into
mainfrom
fix/openrouter-glm53-launch
Open

PierreLeGuen wants to merge 1 commit into
mainfrom
fix/openrouter-glm53-launch

Conversation

@PierreLeGuen

@PierreLeGuen PierreLeGuen commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

The existing /v1/models response uses the legacy flat provider document and includes the entire Cloud catalog. Add a separate public /v1/openrouter/models feed using provider schema 2.4, initially scoped to the active canonical z-ai/glm-5.3-flash model.

The feed derives string USD prices, modality capabilities, identity and limits from the catalog. It stays hidden by default and requires explicit launch/ZDR declarations plus catalog readiness and datacenter metadata. It rejects unsupported pricing/deployment shapes and leaves the existing OpenAI catalog response intact. The ZDR environment variable declares a verified policy; it does not change data handling.

Validation: five focused route/serialization tests, independent validation of the emitted document against the official 2.4 schema, cargo clippy -p api --lib -- -D warnings, formatting and diff checks. No deployment is included.

Related: nearai/cvm-compose-files#253 disables request-body disk buffering in the GLM nginx configuration.

@PierreLeGuen
PierreLeGuen deployed to Cloud API test env September 10, 2026 18:44 — with GitHub Actions Active
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T18:51:28.526719Z 0167951 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ironloopai

ironloopai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review · Status

🟩 Completed

IronLoop completed the review and posted it to GitHub.

Result

Open submitted review →

Run details
  • Run: b0b24ac8-ea45-4246-925c-097f55652860
  • Base: main at 798843f
  • Head: fix/openrouter-glm53-launch at 0167951
  • Created: 2026-09-10 18:49 UTC
  • Updated: 2026-09-10 19:12 UTC

Automatic trigger · attempt 1 of 3 · completed in 22m 19s

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown

Claude Code Review — PR #1041

Reviewed the full diff (openrouter.rs, route wiring, model_with_pricing_to_info visibility, docs, env.example) plus the supporting types (ModelInfo, ModelWithPricing, ModelsServiceTrait, cache_control_layer). No prior human review threads existed on this PR at review time (only a Codex review still in progress).

No blocking correctness bug found. The additive route, is_ready default-false gating, fail-closed metadata checks, and untouched /v1/models response make this rolling-update safe. cache_control_layer only stamps 2xx, so the 503 path is correctly not cacheable. The test double implements every non-default trait method and the ModelWithPricing fixture covers all 30 fields.

Notes in priority order — none are hard blockers, but (1) and (2) are worth fixing before launch.

1. Zero observability on the 503 path (crates/api/src/routes/openrouter.rs:47-79)

Six distinct rejection causes — service error, text_pricing set, non-vllm provider, negative price, cost_per_image != 0, and any of ~8 document() rejections — all collapse into one identical opaque catalog_unavailable 503 with no log line at all. This is a public endpoint OpenRouter polls as a launch gate; when it 503s there is nothing in the logs saying why. Per CLAUDE.md, model names/IDs are explicitly OK to log:

tracing::warn!(
    "OpenRouter catalog rejected launch model: model_name={}, reason={}",
    LAUNCH_MODEL, reason
);

Same for document() returning None — worth threading a &'static str reason out, or at least logging which gate tripped.

2. Config read via raw std::env::var in a route module (openrouter.rs:20-30)

This is the only place in crates/api/src/routes/ that reads std::env::var (verified by grep) — everything else goes through crates/config. Consequences:

  • No startup validation or visibility: OPENROUTER_GLM53_FLASH_ZDR=TRUE, =1, or =yes all silently mean "not declared" and keep the model hidden, with no warning. For a launch flag, a typo produces a silent no-op indistinguishable from a deliberate hold.
  • Bypasses ApiConfig, so the effective values are unobservable and untestable without mutating process env.

Suggest adding the two flags to crates/config/src/types.rs and threading them into build_model_routes, or minimally logging the resolved (ready, zdr) once at startup.

3. Validation is split between the route and document() (openrouter.rs:65-77 vs 84-118)

The provider_type != "vllm", negative-price, and cost_per_image != 0 gates live only in models(). document() is a plain fn the tests already call directly, and on its own it will happily emit a full "verified flat-price" document for a model carrying a per-image surcharge or an external provider. Since the point of these gates is "never project a tiered or external model as this verified configuration", they belong inside document() so the invariant travels with the function. Note text_pricing.is_some() is already checked in both places — the right instinct, just applied inconsistently.

4. is_ready can flap across replicas during a rollout

Env is snapshotted per-process at router construction, and the response carries public, max-age=30, stale-while-revalidate=120. Flipping OPENROUTER_GLM53_FLASH_READY therefore yields a window where some replicas serve is_ready: true and others false, with up to ~150s of cache on top. docs/openrouter-provider.md mentions the restart requirement and the cache delay separately, but not that they compound into nondeterministic is_ready mid-rollout — worth one sentence, given the doc already warns that flipping readiness changes OpenRouter's baseline-test behavior.

5. Confirm intent: missing model returns 200 {"data": []}, not 503

get_models_with_pricing() returns only active models, so deactivating GLM in the catalog publishes an empty provider feed (OpenRouter will de-list), whereas unsupported metadata returns 503 (OpenRouter retries and keeps last known state). omitted_and_incompatible_deployments_do_not_get_advertised asserts the empty-array behaviour, so this looks deliberate — flagging only that the two failure modes have opposite blast radii and it deserves a line in the doc.

6. Minor

  • http_catalog_is_public_glm_only_and_hidden_by_default asserts is_ready == false on the basis that the env vars are absent. It fails for anyone with OPENROUTER_GLM53_FLASH_READY=true exported in their shell. Constructing OpenRouterState explicitly (as the other tests do) removes the ambient dependency.
  • document() is ~100 lines of dense boolean chains with ?-based early exits and terse bindings (hf, input, context, output). Correct, but noticeably denser than the surrounding route code; splitting the eligibility gate from serialization would help future readers.
  • I could not verify the emitted document against the OpenRouter 2.4 schema (no network access in this environment), so I am taking the PR description's independent-validation claim at face value. Worth re-confirming the image input-modality entry specifically, since it emits pricing with unit: "token" and no supported_inputs.

⚠️

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0167951ff3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"stop" => json!({"type": "array"}),
// Variable token-ID keys cannot be represented by a closed object.
"logit_bias" => json!({"type": "unknown"}),
_ => continue,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Advertise the catalog's top_a capability

When the GLM catalog declares top_a—a value accepted by the admin vocabulary and forwarded by the completion path—the wildcard arm silently drops it from supported_parameters. Because this endpoint is OpenRouter's discovery feed, the generated catalog incorrectly reports that the deployment lacks this supported sampling control; handle top_a as a range alongside top_p and min_p.

Useful? React with 👍 / 👎.


/// OpenRouter provider schema 2.4; only the explicitly approved launch model.
/// Readiness is independent of the general Cloud catalog and defaults to false.
#[utoipa::path(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark the discovery operation as public in OpenAPI

The runtime route is deliberately unauthenticated, but ApiDoc installs global session_token/api_key requirements, so this operation inherits them without an explicit security(()) override. Generated clients and the API documentation will therefore require bearer credentials for the public discovery URL; add the same empty security override used by /v1/models.

Useful? React with 👍 / 👎.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review · Summary

Found one low-severity catalog capability omission.

Findings: 🟡 Low 1

Code-specific findings are attached to the diff.

Validation
  • OpenRouter route tests — Five focused route and serialization tests passed.
  • Provider schema 2.4 document — The generated catalog fixture validated against the provider schema.
Review details
  • Run: b0b24ac8-ea45-4246-925c-097f55652860
  • Attempts: 1

"stop" => json!({"type": "array"}),
// Variable token-ID keys cannot be represented by a closed object.
"logit_bias" => json!({"type": "unknown"}),
_ => continue,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Low · Expose top_a when the catalog supports it

top_a is an allowed catalog sampling parameter and is accepted by the completion path, but this conversion falls through to continue. If the launch model declares support for it, the 2.4 document omits the capability, so OpenRouter cannot expose that supported control. Map it to the same range descriptor used for the other continuous sampling parameters.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 OpenCodeReview found 3 issue(s) in this PR.

  • ✅ 3 posted as inline comment(s)
  • 📝 0 posted as summary

⚠️ 1 warning(s) occurred during review.

Comment on lines +58 to +60
.get_models_with_pricing()
.await
.map_err(|_| unavailable())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map_err(|_| unavailable()) silently discards the original error from get_models_with_pricing(), replacing it with a generic "catalog_unavailable" 503. The established convention in the same codebase (completions.rs:1509-1513) logs the original error with tracing::warn! before falling back to a default. Without logging, operators cannot distinguish a DB outage from a transient fault during incident response. Consider logging the error before mapping it to the 503 response.

Suggestion:

Suggested change
.get_models_with_pricing()
.await
.map_err(|_| unavailable())?;
.get_models_with_pricing()
.await
.map_err(|e| {
tracing::warn!(error = %e, "Failed to read model catalog for OpenRouter endpoint");
unavailable()
})?;

return Err(unavailable());
}
let model = super::completions::model_with_pricing_to_info(model);
data.push(document(model, state.ready, state.zdr).ok_or_else(unavailable)?);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When document() returns None for a model that passed the initial model_name == LAUNCH_MODEL filter, the handler returns 503 (via ok_or_else(unavailable)?). This conflates two distinct conditions: (1) the catalog service is genuinely unavailable, and (2) the catalog returned the launch model but its metadata failed validation. A 503 for a metadata validation failure is misleading — the service is up, but the model configuration is non-compliant for this launch contract. Consider logging a distinct warning when document() returns None so operators can diagnose configuration issues versus availability issues.

Suggestion:

Suggested change
data.push(document(model, state.ready, state.zdr).ok_or_else(unavailable)?);
match document(model, state.ready, state.zdr) {
Some(doc) => data.push(doc),
None => {
tracing::warn!(
model = %model.id,
"Launch model present in catalog but failed OpenRouter metadata validation"
);
return Err(unavailable());
}
}

Comment on lines +20 to +23
pub fn from_env(models_service: Arc<dyn ModelsServiceTrait>) -> Self {
Self {
models_service,
ready: std::env::var("OPENROUTER_GLM53_FLASH_READY").as_deref() == Ok("true"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenRouterState::from_env reads OPENROUTER_GLM53_FLASH_READY and OPENROUTER_GLM53_FLASH_ZDR only at router construction time (process startup). Changing these env vars at runtime has no effect without a full process restart. This is acceptable for a launch gate, but consider adding a code comment noting this behavior so operators toggling readiness flags aren't surprised by stale values.

Suggestion:

Suggested change
pub fn from_env(models_service: Arc<dyn ModelsServiceTrait>) -> Self {
Self {
models_service,
ready: std::env::var("OPENROUTER_GLM53_FLASH_READY").as_deref() == Ok("true"),
/// Reads readiness and ZDR flags from the environment at call time.
/// Since this is invoked only during router construction (process startup),
/// changes to these env vars require a full process restart to take effect.
pub fn from_env(models_service: Arc<dyn ModelsServiceTrait>) -> Self {
Self {
models_service,
ready: std::env::var("OPENROUTER_GLM53_FLASH_READY").as_deref() == Ok("true"),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant