Skip to content

feat(requesty): sync models from the Requesty catalog - #1

Open
devin-ai-integration[bot] wants to merge 2 commits into
devfrom
devin/1785257558-requesty-sync
Open

feat(requesty): sync models from the Requesty catalog#1
devin-ai-integration[bot] wants to merge 2 commits into
devfrom
devin/1785257558-requesty-sync

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Adds a sync module for Requesty (packages/core/src/sync/providers/requesty.ts), registered in sync/index.ts under aggregators, so the hand-maintained providers/requesty catalog tracks the gateway's own routes, prices, and limits. This run: 65 created, 19 updated, 0 deleted.

Source: https://router.requesty.ai/v1/models (public; fetched without a key deliberately — a key only adds the account's own policy/ aliases, which are user-configured routes rather than catalog models). Reasoning surface: https://docs.requesty.ai/features/reasoning

Non-obvious decisions, all documented in the new sync.md section:

  • Capability flags may only raise a capability, never clear one. The flags describe the controls the gateway exposes and under-report intrinsic capabilities: xai/grok-4 and deepseek/deepseek-reasoner report supports_reasoning: false, xai/grok-4-fast reports supports_vision: false. So each field is flag === true ? true : existing?.field, and factored models never emit modalities (a single vision flag cannot express the pdf/audio inputs base metadata already records).
  • New files are only created for first-party vendor routes that already have a models/ entry, so every created file is a base_model file and curated facts stay inherited. sourceID returns undefined for other prefixes, and resolveRequestyBaseModel maps the vendor prefix onto metadata (zai/glm-5.2zhipuai/glm-5.2, moonshot/kimi-k3moonshotai/kimi-k3) while rejecting variant routes (openai/gpt-5.4:flex, vertex/claude-sonnet-5@us-east5). Hosting routes (vertex/, bedrock/, deepinfra/, …) belong to the provider catalogs that already cover them; unmatched first-party routes are listed in the sync notice instead of opening issues.
  • reasoning_options is authored-first, then effort + decimal budget for OpenAI/Anthropic/Google routes (the only vendors whose reasoning_effort translation Requesty documents); every other new reasoning model falls back to [] rather than inventing a control.
  • Pricing is USD/token in the catalog and converted to per-1M; zero cache prices are dropped, and authored cost.tiers bands are preserved because the catalog exposes no tier thresholds. The catalog is authoritative for per-route limit.context/limit.output, with max_output_tokens: 0 falling back to the authored output limit.
  • deleteMissing: false: a model can move between prefixes (an openai/ chat route becoming openai-responses/) without being retired, so absent locals are retained and reported.

Verification: bun validate clean, bun test packages/core/test/sync.test.ts 60 pass (5 new Requesty tests), bun models:sync requesty --dry-run idempotent after the write. bun test has 3 failures on dev unrelated to this change (open-weight metadata + sdk snapshot tests).

Note: opened against the requestyai/models.dev fork (whose dev was fast-forwarded to upstream sst/models.dev@52d5045) because Devin has no write access to sst/models.dev — the branch is ready to be sent upstream as-is.

Link to Devin session: https://app.devin.ai/sessions/8dfb3465f4b34a83a0451742cce7aac0
Requested by: @requesty-JohnCosta27

@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration
devin-ai-integrationBotforce-pushed the devin/1785257558-requesty-sync branch from aa90a39 to 3fd9fe1CompareJuly 28, 2026 17:39
// prefixes (an `openai/` chat route becoming an `openai-responses/` route)
// without being retired, so local models absent from a run are reported for
// manual lifecycle review instead of deleted.
deleteMissing: false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

deleteMissing true please

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — the flag is gone, so missing routes are deleted. Heads-up on what that removed: 14 files whose routes moved prefix, e.g. openai/gpt-5-codexopenai-responses/gpt-5-codex (recreated under the new ID), plus anthropic/claude-opus-4 and anthropic/claude-sonnet-4, which the endpoint no longer lists at all.

import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel } from "./openrouter.js";

// Requesty is an OpenAI-compatible LLM gateway. Its public model catalog carries

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove all these extra comments

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stripped — the file is down to a few one-liners where the behaviour isn't obvious from the code.

// documented for OpenAI, Anthropic, and Google/Vertex routes only, so those are
// the vendors whose reasoning controls we can assert on a new model.
// https://docs.requesty.ai/features/reasoning
const REASONING_EFFORT_VENDORS = new Set(["anthropic", "google", "openai"]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

everyone is a reasoning effort vendor, we convert them all, shouldn't I see more models here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You were right — the allowlist is gone, every reasoning model gets the effort option now. Reasoning coverage went from the 3 first-party vendors to every route the API flags (or whose base metadata asserts reasoning).

input_price: z.number(),
output_price: z.number(),
cached_price: z.number().optional(),
caching_price: z.number().optional(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you should use the other Devin PR (which introduced pricing to have an array here actually showing all the bands we have, so we can potentially map it to different tiers

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Wired up: the schema takes the optional pricing array from requestyai/backend#2349 (prompt_tokens_threshold, input_price, output_price, cached_price, caching_price) and maps every band above threshold 0 to a cost.tiers entry, with the threshold-0 band staying the flat cost. The live endpoint doesn't return pricing yet, so today's run keeps the hand-authored bands (e.g. the 200K Anthropic tier); the moment anomalyco#2349 ships they come from the API instead.

Comment on lines +92 to +93
// `models/` metadata entry is enough to bring them into the catalog.
return vendorMetadata(model.id) === undefined ? undefined : model.id;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair — that was doing two things at once. sourceID is just model.id now; it only tells the runner which remote ID to name in the skipped-models notice.

return value !== undefined && value > 0 ? value : undefined;
}

type Modality = "text" | "audio" | "image" | "video" | "pdf";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

/v1/models only currently returns text models (so we don't need to do this stuff)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Dropped — modalities come from the base metadata now, so the Modality type and both helpers are gone.

};
}

function isModality(value: Modality | undefined): value is Modality {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can remove this then

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Gone with it.

return value !== undefined;
}

function reasoningOptions(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's remove the task_budgets, and have reasoning efforts only, that is the same as all the other providers

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — a single effort option, no budget_tokens.

// `deepseek/deepseek-reasoner`) report `supports_reasoning: false`, and some
// multimodal routes report `supports_vision: false`. A flag may raise a
// capability, never clear one that is authored or inherited.
const capabilities = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is an ugly object, construct this better

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Gone — capabilities are supported_parameters entries on the shaped model now, so the shared builder derives attachment / reasoning / tool_call / structured_output.

// Region-pinned (`@us-east5`) and service-tier (`:flex`, `:high`) routes are
// variants of a catalog model rather than models of their own, so they are never
// created from the catalog alone.
export function resolveRequestyBaseModel(modelID: string): string | undefined {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should create the region models, otherwise how would the user use them?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed and fixed. Base metadata is now resolved by model slug with the @region / :tier suffix stripped, so vertex/claude-opus-4-5@us-east5 inherits models/anthropic/claude-opus-4-5.toml and gets its own file at the route ID callers actually pass. That took the catalog from 84 files to ~500 (409 created), covering vertex/, bedrock/, azure/, deepinfra/, coding/, :flex, and the region pins.

@requesty-JohnCosta27requesty-JohnCosta27 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

const built = buildOpenRouterModel(toOpenRouterShape(model), existing);

you can import from the open router shape, so let's do that and keep this file tiny. We want this file to be TINY

@devin-ai-integration
devin-ai-integrationBotforce-pushed the devin/1785257558-requesty-sync branch from 3fd9fe1 to df3807eCompareJuly 28, 2026 17:57
@devin-ai-integration

Copy link
Copy Markdown
Author

Refactored onto the OpenRouter shape as asked — requesty.ts is now 234 lines (was 275 doing its own thing), and the only Requesty-specific code left is the Zod schema, the fetch, the adapter into OpenRouterModel, the pricing-band → cost.tiers mapping, and slug-based base-model resolution. buildOpenRouterModel does modalities, capabilities, description, reasoning options, limits, pricing and factoring.

What changed behaviourally:

  • Region and service-tier routes are real models now (slug lookup with @region / :tier stripped), so vertex/claude-opus-4-5@us-east5 inherits models/anthropic/claude-opus-4-5.toml. Catalog goes 84 → ~500 files (409 created, 75 updated, 14 deleted).
  • deleteMissing is on; effort-only reasoning_options for every reasoning route; no policy/api filtering; capability flags may only raise a capability the base metadata already asserts (the API reports supports_reasoning: false for always-on reasoning models).
  • pricing bands from requestyai/backend#2349 map to cost.tiers; the endpoint doesn't return them yet, so today's files keep the authored bands.

Still skipped: 95 routes with no models/ metadata entry (novita/qwen/..., xai/grok-4, ...) — they're listed in the sync notice, and an authored file for one of them is left untouched rather than deleted.

Two commits as before: script then catalog. bun validate clean, sync tests 59/59.

name: "Requesty",
modelsDir: "providers/requesty/models",
sourceID: (model) => model.id,
skippedNotice,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nevermind, lets inline the skippedNotice, but make it small

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Inlined, two lines.

// Requesty routes carry gateway facts (price, limits, capabilities) but no
// release dates, weights, or family. Without a `models/` entry to inherit
// those from, an authored file is left as it is and the route is reported.
if (baseModel === undefined) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what does this mean?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry, that comment was doing too much work. It's the routes we have no models/<lab>/<slug>.toml entry for (95 of them today: novita/qwen/qwen3-235b-a22b-fp8, xai/grok-4, …). There's nothing to inherit name / release_date / open_weights from, and the API doesn't give them, so the route can't be written from scratch. If someone already hand-wrote the file, it's left alone (otherwise deleteMissing would delete it); otherwise it's listed in the skipped notice. Comment is one line now: "A route with no metadata to inherit keeps whatever is authored for it."

/** Context-length pricing bands, cheapest threshold first, excluding the base band. */
function pricingTiers(model: RequestyModel): NonNullable<SyncedFullModel["cost"]>["tiers"] {
const tiers = (model.pricing ?? [])
.filter((band) => band.prompt_tokens_threshold > 0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remove the filter and the sort, not needed because we return them this way already

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — no filter, no sort, just .slice(1) to skip the base band (which stays the flat cost).

const rest = ids.length - listed.length;
return [
`${ids.length} Requesty models were skipped because no \`models/\` metadata entry matches them yet.`,
`Skipped remote IDs: ${listed.map((id) => `\`${id}\``).join(", ")}${rest > 0 ? `, and ${rest} more` : ""}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lets not have all these strange joins

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Trimmed to one line and a single join: ${ids.length} Requesty routes have no \models/` metadata entry yet: ${ids.join(", ")}`.

* (`vertex/claude-opus-4@us-east5`) or a service tier (`openai/gpt-5.4:flex`), so
* the metadata entry is looked up by model slug rather than by route prefix.
*/
export function resolveRequestyBaseModel(modelID: string): string | undefined {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we care?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Because the route prefix is the host, not the lab: vertex/claude-opus-4-5@us-east5, bedrock/claude-opus-4-5, and anthropic/claude-opus-4-5 all need to inherit models/anthropic/claude-opus-4-5.toml, so the lookup is on the slug with @region/:tier stripped rather than on the prefix. That's the only reason the function exists — docstring is one line now.

devin-ai-integrationBotand others added 2 commits July 28, 2026 18:18
Co-Authored-By: john <john@requesty.ai>
Co-Authored-By: john <john@requesty.ai>
@devin-ai-integration
devin-ai-integrationBotforce-pushed the devin/1785257558-requesty-sync branch from eca596c to d2187b4CompareJuly 28, 2026 18:18
Sign up for freeto 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

@requesty-JohnCosta27