diff --git a/README.md b/README.md index dd2d095..8dd2f0c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ credentials, or make network requests. Promptkit owns catalog parsing, validation, runtime behavior, credential handling, and consumer documentation. This module owns the OpenRouter backend -manifest and its maintained profile data. Consumers should use versions chosen +definition and its maintained profile data. Consumers should use versions chosen by their Promptkit release rather than treating this module as a standalone runtime integration. @@ -16,17 +16,25 @@ and documentation. ## Compatibility -This compatibility policy applies to the `/v2` module. Catalog profiles track +This compatibility policy applies to the `/v3` module. Catalog profiles track upstream availability and may be added, removed, renamed, or modified without deprecation or advance notice. Consumers that need a particular profile set should use the catalog version selected by their Promptkit release. -Backend definitions, the exported Go API, and the catalog manifest schema do +Backend definitions, the exported Go API, and the catalog asset format do not share that profile-data exception. Their compatibility follows the normal -semantic-versioning rules. Published v1 releases retain their historical -contract; this README does not retroactively alter it. +semantic-versioning rules. Published v1 and `/v2` releases retain their historical +contracts; this README does not retroactively alter them. -Catalog releases use dated versions in the form `v2.YYYYMMDD.N`. The date +The `/v3` module replaces the `/v2` manifest and flat profile documents with +ordinary keyed ConfigKit inputs: `catalog/backend.yml` defines +`backends.openrouter`, and each file under `catalog/profiles/` defines one +literal ID beneath `profiles`. Profile inheritance uses `extends` instead of +`base_profile`. This asset-format change is incompatible with `/v2` consumers; +they should remain on a published `/v2` version. The exported `FS` and `Root` +remain the asset access surface. + +Catalog releases use dated versions in the form `v3.YYYYMMDD.N`. The date identifies when the catalog snapshot was published in UTC; it does not certify that a provider currently offers a model or endpoint named by the data. diff --git a/catalog/backend.json b/catalog/backend.json deleted file mode 100644 index 837cb7a..0000000 --- a/catalog/backend.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schema_version": 1, - "id": "openrouter", - "endpoint": "https://openrouter.ai/api/v1", - "api_key_env": "OPENROUTER_API_KEY", - "concurrency_limit": 16, - "queue_capacity": 1024, - "extra_params": null -} diff --git a/catalog/backend.yml b/catalog/backend.yml new file mode 100644 index 0000000..b4ff7e3 --- /dev/null +++ b/catalog/backend.yml @@ -0,0 +1,6 @@ +backends: + openrouter: + endpoint: https://openrouter.ai/api/v1 + api_key_env: OPENROUTER_API_KEY + concurrency_limit: 16 + queue_capacity: 1024 diff --git a/catalog/profiles/aion-labs/aion-2.yml b/catalog/profiles/aion-labs/aion-2.yml index a22cd8f..13d88b2 100644 --- a/catalog/profiles/aion-labs/aion-2.yml +++ b/catalog/profiles/aion-labs/aion-2.yml @@ -1,8 +1,9 @@ -id: aion-2 -backend: openrouter -model: aion-labs/aion-2.0 -temperature: 0.72 -reasoning_effort: high -top_p: 0.95 -timeout_seconds: 180 -service_tier: flex +profiles: + aion-2: + backend: openrouter + model: aion-labs/aion-2.0 + temperature: 0.72 + reasoning_effort: high + top_p: 0.95 + timeout_seconds: 180 + service_tier: flex diff --git a/catalog/profiles/aion-labs/aion-3-0-mini.yml b/catalog/profiles/aion-labs/aion-3-0-mini.yml index 17b1f5b..268b3f0 100644 --- a/catalog/profiles/aion-labs/aion-3-0-mini.yml +++ b/catalog/profiles/aion-labs/aion-3-0-mini.yml @@ -1,4 +1,5 @@ -id: aion-3-0-mini -backend: openrouter -model: "aion-labs/aion-3.0-mini" -service_tier: flex +profiles: + aion-3-0-mini: + backend: openrouter + model: "aion-labs/aion-3.0-mini" + service_tier: flex diff --git a/catalog/profiles/aion-labs/aion-3-0.yml b/catalog/profiles/aion-labs/aion-3-0.yml index 8cd3f7c..03756b4 100644 --- a/catalog/profiles/aion-labs/aion-3-0.yml +++ b/catalog/profiles/aion-labs/aion-3-0.yml @@ -1,4 +1,5 @@ -id: aion-3-0 -backend: openrouter -model: "aion-labs/aion-3.0" -service_tier: flex +profiles: + aion-3-0: + backend: openrouter + model: "aion-labs/aion-3.0" + service_tier: flex diff --git a/catalog/profiles/anthropic/claude-fable-latest.yml b/catalog/profiles/anthropic/claude-fable-latest.yml index 0affde6..5168630 100644 --- a/catalog/profiles/anthropic/claude-fable-latest.yml +++ b/catalog/profiles/anthropic/claude-fable-latest.yml @@ -1,6 +1,7 @@ -id: claude-fable-latest -backend: openrouter -model: "~anthropic/claude-fable-latest" -reasoning_effort: high -timeout_seconds: 600 -service_tier: flex +profiles: + claude-fable-latest: + backend: openrouter + model: "~anthropic/claude-fable-latest" + reasoning_effort: high + timeout_seconds: 600 + service_tier: flex diff --git a/catalog/profiles/anthropic/claude-haiku-latest.yml b/catalog/profiles/anthropic/claude-haiku-latest.yml index e5ada35..79110c5 100644 --- a/catalog/profiles/anthropic/claude-haiku-latest.yml +++ b/catalog/profiles/anthropic/claude-haiku-latest.yml @@ -1,6 +1,7 @@ -id: claude-haiku-latest -backend: openrouter -model: "~anthropic/claude-haiku-latest" -reasoning_effort: medium -timeout_seconds: 240 -service_tier: flex +profiles: + claude-haiku-latest: + backend: openrouter + model: "~anthropic/claude-haiku-latest" + reasoning_effort: medium + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/anthropic/claude-opus-latest.yml b/catalog/profiles/anthropic/claude-opus-latest.yml index 6d628ac..512ee2a 100644 --- a/catalog/profiles/anthropic/claude-opus-latest.yml +++ b/catalog/profiles/anthropic/claude-opus-latest.yml @@ -1,6 +1,7 @@ -id: claude-opus-latest -backend: openrouter -model: "~anthropic/claude-opus-latest" -reasoning_effort: high -timeout_seconds: 240 -service_tier: flex +profiles: + claude-opus-latest: + backend: openrouter + model: "~anthropic/claude-opus-latest" + reasoning_effort: high + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/anthropic/claude-sonnet-latest.yml b/catalog/profiles/anthropic/claude-sonnet-latest.yml index adce8d6..373cb11 100644 --- a/catalog/profiles/anthropic/claude-sonnet-latest.yml +++ b/catalog/profiles/anthropic/claude-sonnet-latest.yml @@ -1,6 +1,7 @@ -id: claude-sonnet-latest -backend: openrouter -model: "~anthropic/claude-sonnet-latest" -reasoning_effort: high -timeout_seconds: 240 -service_tier: flex +profiles: + claude-sonnet-latest: + backend: openrouter + model: "~anthropic/claude-sonnet-latest" + reasoning_effort: high + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/deepseek/deepseek-flash-latest.yml b/catalog/profiles/deepseek/deepseek-flash-latest.yml index 4e176e9..b76cc53 100644 --- a/catalog/profiles/deepseek/deepseek-flash-latest.yml +++ b/catalog/profiles/deepseek/deepseek-flash-latest.yml @@ -1,4 +1,5 @@ -id: deepseek-flash-latest -backend: openrouter -model: "~deepseek/deepseek-flash-latest" -service_tier: flex +profiles: + deepseek-flash-latest: + backend: openrouter + model: "~deepseek/deepseek-flash-latest" + service_tier: flex diff --git a/catalog/profiles/deepseek/deepseek-pro-latest.yml b/catalog/profiles/deepseek/deepseek-pro-latest.yml index a36858b..07d9350 100644 --- a/catalog/profiles/deepseek/deepseek-pro-latest.yml +++ b/catalog/profiles/deepseek/deepseek-pro-latest.yml @@ -1,4 +1,5 @@ -id: deepseek-pro-latest -backend: openrouter -model: "~deepseek/deepseek-pro-latest" -service_tier: flex +profiles: + deepseek-pro-latest: + backend: openrouter + model: "~deepseek/deepseek-pro-latest" + service_tier: flex diff --git a/catalog/profiles/google/gemini-3-5-flash-lite.yml b/catalog/profiles/google/gemini-3-5-flash-lite.yml index 5758c9c..683365e 100644 --- a/catalog/profiles/google/gemini-3-5-flash-lite.yml +++ b/catalog/profiles/google/gemini-3-5-flash-lite.yml @@ -1,4 +1,5 @@ -id: gemini-3-5-flash-lite -backend: openrouter -model: "google/gemini-3.5-flash-lite" -service_tier: flex +profiles: + gemini-3-5-flash-lite: + backend: openrouter + model: "google/gemini-3.5-flash-lite" + service_tier: flex diff --git a/catalog/profiles/google/gemini-3-5-flash.yml b/catalog/profiles/google/gemini-3-5-flash.yml index b6c9430..24f53f1 100644 --- a/catalog/profiles/google/gemini-3-5-flash.yml +++ b/catalog/profiles/google/gemini-3-5-flash.yml @@ -1,4 +1,5 @@ -id: gemini-3-5-flash -backend: openrouter -model: "google/gemini-3.5-flash" -service_tier: flex +profiles: + gemini-3-5-flash: + backend: openrouter + model: "google/gemini-3.5-flash" + service_tier: flex diff --git a/catalog/profiles/google/gemini-3-6-flash.yml b/catalog/profiles/google/gemini-3-6-flash.yml index bafe035..94416a5 100644 --- a/catalog/profiles/google/gemini-3-6-flash.yml +++ b/catalog/profiles/google/gemini-3-6-flash.yml @@ -1,4 +1,5 @@ -id: gemini-3-6-flash -backend: openrouter -model: "google/gemini-3.6-flash" -service_tier: flex +profiles: + gemini-3-6-flash: + backend: openrouter + model: "google/gemini-3.6-flash" + service_tier: flex diff --git a/catalog/profiles/google/gemini-3-7-flash.yml b/catalog/profiles/google/gemini-3-7-flash.yml index 12eb212..a290701 100644 --- a/catalog/profiles/google/gemini-3-7-flash.yml +++ b/catalog/profiles/google/gemini-3-7-flash.yml @@ -1,4 +1,5 @@ -id: gemini-3-7-flash -backend: openrouter -model: "google/gemini-3.7-flash" -service_tier: flex +profiles: + gemini-3-7-flash: + backend: openrouter + model: "google/gemini-3.7-flash" + service_tier: flex diff --git a/catalog/profiles/google/gemini-3-8-flash.yml b/catalog/profiles/google/gemini-3-8-flash.yml index 7fa76b4..9732ac5 100644 --- a/catalog/profiles/google/gemini-3-8-flash.yml +++ b/catalog/profiles/google/gemini-3-8-flash.yml @@ -1,4 +1,5 @@ -id: gemini-3-8-flash -backend: openrouter -model: "google/gemini-3.8-flash" -service_tier: flex +profiles: + gemini-3-8-flash: + backend: openrouter + model: "google/gemini-3.8-flash" + service_tier: flex diff --git a/catalog/profiles/google/gemini-3-flash-lite.yml b/catalog/profiles/google/gemini-3-flash-lite.yml index 4ff439b..004e215 100644 --- a/catalog/profiles/google/gemini-3-flash-lite.yml +++ b/catalog/profiles/google/gemini-3-flash-lite.yml @@ -1,8 +1,9 @@ -id: gemini-3-flash-lite -backend: openrouter -model: "google/gemini-3.1-flash-lite" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -service_tier: flex +profiles: + gemini-3-flash-lite: + backend: openrouter + model: "google/gemini-3.1-flash-lite" + #temperature: 0.15 + reasoning_effort: high + #top_p: 0.98 + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/google/gemini-flash-latest.yml b/catalog/profiles/google/gemini-flash-latest.yml index 89acd90..7339db5 100644 --- a/catalog/profiles/google/gemini-flash-latest.yml +++ b/catalog/profiles/google/gemini-flash-latest.yml @@ -1,8 +1,9 @@ -id: gemini-flash-latest -backend: openrouter -model: "~google/gemini-flash-latest" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -service_tier: flex +profiles: + gemini-flash-latest: + backend: openrouter + model: "~google/gemini-flash-latest" + #temperature: 0.15 + reasoning_effort: high + #top_p: 0.98 + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/google/gemini-pro-latest.yml b/catalog/profiles/google/gemini-pro-latest.yml index 2ce5303..f19e279 100644 --- a/catalog/profiles/google/gemini-pro-latest.yml +++ b/catalog/profiles/google/gemini-pro-latest.yml @@ -1,8 +1,9 @@ -id: gemini-pro-latest -backend: openrouter -model: "~google/gemini-pro-latest" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -service_tier: flex +profiles: + gemini-pro-latest: + backend: openrouter + model: "~google/gemini-pro-latest" + #temperature: 0.15 + reasoning_effort: high + #top_p: 0.98 + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/google/gemma-4-31b.yml b/catalog/profiles/google/gemma-4-31b.yml index 39c3529..a818b4f 100644 --- a/catalog/profiles/google/gemma-4-31b.yml +++ b/catalog/profiles/google/gemma-4-31b.yml @@ -1,8 +1,9 @@ -id: gemma-4-31b -backend: openrouter -model: google/gemma-4-31b-it:exacto -temperature: 0.15 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 240 -service_tier: flex +profiles: + gemma-4-31b: + backend: openrouter + model: google/gemma-4-31b-it:exacto + temperature: 0.15 + reasoning_effort: high + top_p: 0.98 + timeout_seconds: 240 + service_tier: flex diff --git a/catalog/profiles/minimax/minimax-m2.yml b/catalog/profiles/minimax/minimax-m2.yml index 385a181..eb15092 100644 --- a/catalog/profiles/minimax/minimax-m2.yml +++ b/catalog/profiles/minimax/minimax-m2.yml @@ -1,8 +1,9 @@ -id: minimax-m2 -backend: openrouter -model: minimax/minimax-m2.5 -temperature: 0.5 -reasoning_effort: high -top_p: 0.95 -timeout_seconds: 180 -service_tier: flex +profiles: + minimax-m2: + backend: openrouter + model: minimax/minimax-m2.5 + temperature: 0.5 + reasoning_effort: high + top_p: 0.95 + timeout_seconds: 180 + service_tier: flex diff --git a/catalog/profiles/minimax/minimax-m3.yml b/catalog/profiles/minimax/minimax-m3.yml index c0c1bd0..e67d418 100644 --- a/catalog/profiles/minimax/minimax-m3.yml +++ b/catalog/profiles/minimax/minimax-m3.yml @@ -1,8 +1,9 @@ -id: minimax-m3 -backend: openrouter -model: minimax/minimax-m3 -#temperature: 0.5 -reasoning_effort: high -#top_p: 0.95 -timeout_seconds: 180 -service_tier: flex +profiles: + minimax-m3: + backend: openrouter + model: minimax/minimax-m3 + #temperature: 0.5 + reasoning_effort: high + #top_p: 0.95 + timeout_seconds: 180 + service_tier: flex diff --git a/catalog/profiles/mistral/mistral-large-2512.yml b/catalog/profiles/mistral/mistral-large-2512.yml index c77c9d8..9abdd7a 100644 --- a/catalog/profiles/mistral/mistral-large-2512.yml +++ b/catalog/profiles/mistral/mistral-large-2512.yml @@ -1,6 +1,7 @@ -id: mistral-large-2512 -backend: openrouter -model: mistralai/mistral-large-2512 -temperature: 0.15 -top_p: 0.98 -timeout_seconds: 180 +profiles: + mistral-large-2512: + backend: openrouter + model: mistralai/mistral-large-2512 + temperature: 0.15 + top_p: 0.98 + timeout_seconds: 180 diff --git a/catalog/profiles/mistral/mistral-medium-3-5.yml b/catalog/profiles/mistral/mistral-medium-3-5.yml index b5201ca..2f35c07 100644 --- a/catalog/profiles/mistral/mistral-medium-3-5.yml +++ b/catalog/profiles/mistral/mistral-medium-3-5.yml @@ -1,7 +1,8 @@ -id: mistral-medium-3-5 -backend: openrouter -model: mistralai/mistral-medium-3-5 -temperature: 0.15 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 180 +profiles: + mistral-medium-3-5: + backend: openrouter + model: mistralai/mistral-medium-3-5 + temperature: 0.15 + reasoning_effort: high + top_p: 0.98 + timeout_seconds: 180 diff --git a/catalog/profiles/mistral/mistral-small-3.yml b/catalog/profiles/mistral/mistral-small-3.yml index 7e01e55..f14360d 100644 --- a/catalog/profiles/mistral/mistral-small-3.yml +++ b/catalog/profiles/mistral/mistral-small-3.yml @@ -1,6 +1,7 @@ -id: mistral-small-3 -backend: openrouter -model: mistralai/mistral-small-3.2-24b-instruct -temperature: 0.05 -top_p: 1.0 -timeout_seconds: 180 +profiles: + mistral-small-3: + backend: openrouter + model: mistralai/mistral-small-3.2-24b-instruct + temperature: 0.05 + top_p: 1.0 + timeout_seconds: 180 diff --git a/catalog/profiles/mistral/mistral-small-4.yml b/catalog/profiles/mistral/mistral-small-4.yml index 2d9b44c..7e5d66a 100644 --- a/catalog/profiles/mistral/mistral-small-4.yml +++ b/catalog/profiles/mistral/mistral-small-4.yml @@ -1,7 +1,8 @@ -id: mistral-small-4 -backend: openrouter -model: mistralai/mistral-small-2603 -temperature: 0.1 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 180 +profiles: + mistral-small-4: + backend: openrouter + model: mistralai/mistral-small-2603 + temperature: 0.1 + reasoning_effort: high + top_p: 0.98 + timeout_seconds: 180 diff --git a/catalog/profiles/nvidia/nemotron-3-ultra.yml b/catalog/profiles/nvidia/nemotron-3-ultra.yml index 90f84f3..4199712 100644 --- a/catalog/profiles/nvidia/nemotron-3-ultra.yml +++ b/catalog/profiles/nvidia/nemotron-3-ultra.yml @@ -1,6 +1,7 @@ -id: nemotron-3-ultra -backend: openrouter -model: nvidia/nemotron-3-ultra-550b-a55b -reasoning_effort: high -timeout_seconds: 180 -service_tier: flex +profiles: + nemotron-3-ultra: + backend: openrouter + model: nvidia/nemotron-3-ultra-550b-a55b + reasoning_effort: high + timeout_seconds: 180 + service_tier: flex diff --git a/catalog/profiles/openai/gpt-5-6-luna.yml b/catalog/profiles/openai/gpt-5-6-luna.yml index af0ff09..cac7c8e 100644 --- a/catalog/profiles/openai/gpt-5-6-luna.yml +++ b/catalog/profiles/openai/gpt-5-6-luna.yml @@ -1,4 +1,5 @@ -id: gpt-5-6-luna -backend: openrouter -model: "openai/gpt-5.6-luna" -service_tier: flex +profiles: + gpt-5-6-luna: + backend: openrouter + model: "openai/gpt-5.6-luna" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-5-6-sol.yml b/catalog/profiles/openai/gpt-5-6-sol.yml index 177cbc2..daf78ba 100644 --- a/catalog/profiles/openai/gpt-5-6-sol.yml +++ b/catalog/profiles/openai/gpt-5-6-sol.yml @@ -1,4 +1,5 @@ -id: gpt-5-6-sol -backend: openrouter -model: "openai/gpt-5.6-sol" -service_tier: flex +profiles: + gpt-5-6-sol: + backend: openrouter + model: "openai/gpt-5.6-sol" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-5-6-terra.yml b/catalog/profiles/openai/gpt-5-6-terra.yml index 637f2b7..db61eae 100644 --- a/catalog/profiles/openai/gpt-5-6-terra.yml +++ b/catalog/profiles/openai/gpt-5-6-terra.yml @@ -1,4 +1,5 @@ -id: gpt-5-6-terra -backend: openrouter -model: "openai/gpt-5.6-terra" -service_tier: flex +profiles: + gpt-5-6-terra: + backend: openrouter + model: "openai/gpt-5.6-terra" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-6-astra.yml b/catalog/profiles/openai/gpt-6-astra.yml index 83cc939..96c027e 100644 --- a/catalog/profiles/openai/gpt-6-astra.yml +++ b/catalog/profiles/openai/gpt-6-astra.yml @@ -1,4 +1,5 @@ -id: gpt-6-astra -backend: openrouter -model: "openai/gpt-6-astra" -service_tier: flex +profiles: + gpt-6-astra: + backend: openrouter + model: "openai/gpt-6-astra" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-astra-latest.yml b/catalog/profiles/openai/gpt-astra-latest.yml index 0ca7462..771c9d4 100644 --- a/catalog/profiles/openai/gpt-astra-latest.yml +++ b/catalog/profiles/openai/gpt-astra-latest.yml @@ -1,4 +1,5 @@ -id: gpt-astra-latest -backend: openrouter -model: "~openai/gpt-astra-latest" -service_tier: flex +profiles: + gpt-astra-latest: + backend: openrouter + model: "~openai/gpt-astra-latest" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-luna-latest.yml b/catalog/profiles/openai/gpt-luna-latest.yml index ca59a84..db408dd 100644 --- a/catalog/profiles/openai/gpt-luna-latest.yml +++ b/catalog/profiles/openai/gpt-luna-latest.yml @@ -1,4 +1,5 @@ -id: gpt-luna-latest -backend: openrouter -model: "~openai/gpt-luna-latest" -service_tier: flex +profiles: + gpt-luna-latest: + backend: openrouter + model: "~openai/gpt-luna-latest" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-sol-latest.yml b/catalog/profiles/openai/gpt-sol-latest.yml index 63af353..5ecf002 100644 --- a/catalog/profiles/openai/gpt-sol-latest.yml +++ b/catalog/profiles/openai/gpt-sol-latest.yml @@ -1,4 +1,5 @@ -id: gpt-sol-latest -backend: openrouter -model: "~openai/gpt-sol-latest" -service_tier: flex +profiles: + gpt-sol-latest: + backend: openrouter + model: "~openai/gpt-sol-latest" + service_tier: flex diff --git a/catalog/profiles/openai/gpt-terra-latest.yml b/catalog/profiles/openai/gpt-terra-latest.yml index 8b86ad0..b6c81aa 100644 --- a/catalog/profiles/openai/gpt-terra-latest.yml +++ b/catalog/profiles/openai/gpt-terra-latest.yml @@ -1,4 +1,5 @@ -id: gpt-terra-latest -backend: openrouter -model: "~openai/gpt-terra-latest" -service_tier: flex +profiles: + gpt-terra-latest: + backend: openrouter + model: "~openai/gpt-terra-latest" + service_tier: flex diff --git a/catalog/profiles/x-ai/grok-latest.yml b/catalog/profiles/x-ai/grok-latest.yml index 2e6e840..c6806ea 100644 --- a/catalog/profiles/x-ai/grok-latest.yml +++ b/catalog/profiles/x-ai/grok-latest.yml @@ -1,4 +1,5 @@ -id: grok-latest -backend: openrouter -model: "~x-ai/grok-latest" -service_tier: flex +profiles: + grok-latest: + backend: openrouter + model: "~x-ai/grok-latest" + service_tier: flex diff --git a/catalog/profiles/z-ai/glm-flash-latest.yml b/catalog/profiles/z-ai/glm-flash-latest.yml index 6ea13aa..c1977de 100644 --- a/catalog/profiles/z-ai/glm-flash-latest.yml +++ b/catalog/profiles/z-ai/glm-flash-latest.yml @@ -1,4 +1,5 @@ -id: glm-flash-latest -backend: openrouter -model: "~z-ai/glm-flash-latest" -service_tier: flex +profiles: + glm-flash-latest: + backend: openrouter + model: "~z-ai/glm-flash-latest" + service_tier: flex diff --git a/catalog/profiles/z-ai/glm-latest.yml b/catalog/profiles/z-ai/glm-latest.yml index bd5e227..5c58bcd 100644 --- a/catalog/profiles/z-ai/glm-latest.yml +++ b/catalog/profiles/z-ai/glm-latest.yml @@ -1,4 +1,5 @@ -id: glm-latest -backend: openrouter -model: "~z-ai/glm-latest" -service_tier: flex +profiles: + glm-latest: + backend: openrouter + model: "~z-ai/glm-latest" + service_tier: flex diff --git a/catalog_test.go b/catalog_test.go index 645b1d1..6d3b217 100644 --- a/catalog_test.go +++ b/catalog_test.go @@ -2,11 +2,11 @@ package openrouter import ( "bytes" - "encoding/json" "errors" "fmt" "io" "io/fs" + "path" "strings" "testing" @@ -14,188 +14,145 @@ import ( ) func TestCatalogLayoutAndContents(t *testing.T) { - fsys := FS() - if fsys == nil { + assets := FS() + if assets == nil { t.Fatal("FS returned nil") } - - manifestPath := Root + "/backend.json" - manifestData, err := fs.ReadFile(fsys, manifestPath) + backendPath := Root + "/backend.yml" + backendData, err := fs.ReadFile(assets, backendPath) if err != nil { - t.Fatalf("read manifest: %v", err) + t.Fatalf("read backend: %v", err) } - assertOpenRouterManifest(t, manifestData) + assertBackend(t, backendData) profilesRoot := Root + "/profiles" - profileCount := 0 - ids := map[string]string{} - err = fs.WalkDir(fsys, Root, func(path string, entry fs.DirEntry, walkErr error) error { + ids := make(map[string]string) + err = fs.WalkDir(assets, Root, func(assetPath string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } - if path == Root || path == profilesRoot || entry.IsDir() { + if assetPath == Root || assetPath == profilesRoot || entry.IsDir() { return nil } - if path == manifestPath { + if assetPath == backendPath { if !entry.Type().IsRegular() { - return fmt.Errorf("manifest is not a regular file") + return fmt.Errorf("backend asset is not a regular file") } return nil } - if !strings.HasPrefix(path, profilesRoot+"/") { - return fmt.Errorf("unexpected catalog entry %q", path) + if !strings.HasPrefix(assetPath, profilesRoot+"/") { + return fmt.Errorf("unexpected catalog entry %q", assetPath) } - if !entry.Type().IsRegular() || !strings.HasSuffix(path, ".yml") { - return fmt.Errorf("profile asset %q is not a regular .yml file", path) + if !entry.Type().IsRegular() || !strings.HasSuffix(assetPath, ".yml") { + return fmt.Errorf("profile asset %q is not a regular .yml file", assetPath) } - - data, err := fs.ReadFile(fsys, path) + data, err := fs.ReadFile(assets, assetPath) if err != nil { return err } - id := assertOpenRouterProfile(t, path, data) + id := assertProfile(t, assetPath, data) if previous, found := ids[id]; found { - return fmt.Errorf("duplicate profile ID %q in %s and %s", id, previous, path) + return fmt.Errorf("duplicate profile ID %q in %s and %s", id, previous, assetPath) } - ids[id] = path - profileCount++ + ids[id] = assetPath return nil }) if err != nil { t.Fatalf("validate catalog layout: %v", err) } - if profileCount == 0 { - t.Fatal("catalog contains no profiles") + if len(ids) != 36 { + t.Fatalf("profile inventory changed: got %d, want 36", len(ids)) } } -func assertOpenRouterManifest(t *testing.T, data []byte) { +func assertBackend(t *testing.T, data []byte) { t.Helper() - - type manifest struct { - SchemaVersion int `json:"schema_version"` - ID string `json:"id"` - Endpoint string `json:"endpoint"` - APIKeyEnv string `json:"api_key_env"` - ConcurrencyLimit int `json:"concurrency_limit"` - QueueCapacity int `json:"queue_capacity"` - ExtraParams json.RawMessage `json:"extra_params"` + root := decodeMapping(t, "backend", data) + backends := requireSingleMapping(t, "backend", root, "backends") + if len(backends) != 1 || backends["openrouter"] == nil { + t.Fatalf("backend asset must define only openrouter: %v", backends) } - - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - var value manifest - if err := decoder.Decode(&value); err != nil { - t.Fatalf("decode manifest: %v", err) - } - if err := requireJSONEnd(decoder); err != nil { - t.Fatalf("decode manifest: %v", err) - } - if value.SchemaVersion != 1 || - value.ID != "openrouter" || - value.Endpoint != "https://openrouter.ai/api/v1" || - value.APIKeyEnv != "OPENROUTER_API_KEY" || - value.ConcurrencyLimit != 16 || - value.QueueCapacity != 1024 || - string(value.ExtraParams) != "null" { - t.Fatalf("unexpected manifest: %#v", value) - } -} - -func requireJSONEnd(decoder *json.Decoder) error { - var trailing any - if err := decoder.Decode(&trailing); errors.Is(err, io.EOF) { - return nil - } else if err != nil { - return err - } - return errors.New("catalog manifest must contain exactly one JSON value") -} - -func assertOpenRouterProfile(t *testing.T, path string, data []byte) string { - t.Helper() - - var document yaml.Node - decoder := yaml.NewDecoder(bytes.NewReader(data)) - if err := decoder.Decode(&document); err != nil { - t.Fatalf("decode profile %s: %v", path, err) - } - if err := requireYAMLEnd(decoder); err != nil { - t.Fatalf("decode profile %s: %v", path, err) - } - if document.Kind != yaml.DocumentNode || len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode { - t.Fatalf("profile %s must contain one mapping document", path) - } - - fields := yamlFields(document.Content[0]) - idField := fields["id"] - if idField == nil { - t.Fatalf("profile %s has no ID", path) - } - id := strings.TrimSpace(idField.Value) - if id == "" { - t.Fatalf("profile %s has a blank ID", path) - } - if fields["backend"] == nil || fields["backend"].Value != "openrouter" { - t.Fatalf("profile %s does not select openrouter", path) - } - for _, field := range []string{"endpoint", "api_key_env", "api_key"} { - if fields[field] != nil { - t.Fatalf("profile %s contains prohibited %s field", path, field) + definition := mappingFields(t, "backend", backends["openrouter"]) + for field, want := range map[string]string{ + "endpoint": "https://openrouter.ai/api/v1", "api_key_env": "OPENROUTER_API_KEY", + "concurrency_limit": "16", "queue_capacity": "1024", + } { + value := definition[field] + if value == nil || value.Value != want { + t.Fatalf("backend %s: got %v, want %q", field, value, want) } } - if extraParams := fields["extra_params"]; extraParams != nil { - assertNoSecretKeys(t, path, extraParams) + if len(definition) != 4 { + t.Fatalf("unexpected backend fields: %v", definition) } - return id } -func requireYAMLEnd(decoder *yaml.Decoder) error { +func assertProfile(t *testing.T, assetPath string, data []byte) string { + t.Helper() + root := decodeMapping(t, assetPath, data) + profiles := requireSingleMapping(t, assetPath, root, "profiles") + if len(profiles) != 1 { + t.Fatalf("profile asset %s must define exactly one ID", assetPath) + } + for id, entry := range profiles { + if id == "" || id != strings.TrimSuffix(path.Base(assetPath), ".yml") { + t.Fatalf("profile asset %s has unexpected ID %q", assetPath, id) + } + fields := mappingFields(t, assetPath, entry) + if fields["backend"] == nil || fields["backend"].Value != "openrouter" { + t.Fatalf("profile %s does not select openrouter", assetPath) + } + if fields["model"] == nil || strings.TrimSpace(fields["model"].Value) == "" { + t.Fatalf("profile %s has no model", assetPath) + } + for _, obsolete := range []string{"id", "base_profile"} { + if fields[obsolete] != nil { + t.Fatalf("profile %s contains obsolete %s field", assetPath, obsolete) + } + } + return id + } + panic("unreachable") +} + +func decodeMapping(t *testing.T, name string, data []byte) *yaml.Node { + t.Helper() + decoder := yaml.NewDecoder(bytes.NewReader(data)) + var document yaml.Node + if err := decoder.Decode(&document); err != nil { + t.Fatalf("decode %s: %v", name, err) + } var trailing yaml.Node - if err := decoder.Decode(&trailing); errors.Is(err, io.EOF) { - return nil - } else if err != nil { - return err + if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { + t.Fatalf("%s must contain exactly one YAML document: %v", name, err) } - return errors.New("profile must contain exactly one YAML document") + if document.Kind != yaml.DocumentNode || len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode { + t.Fatalf("%s must contain one mapping document", name) + } + return document.Content[0] } -func yamlFields(mapping *yaml.Node) map[string]*yaml.Node { - fields := make(map[string]*yaml.Node, len(mapping.Content)/2) - for index := 0; index+1 < len(mapping.Content); index += 2 { - fields[mapping.Content[index].Value] = mapping.Content[index+1] +func requireSingleMapping(t *testing.T, name string, parent *yaml.Node, key string) map[string]*yaml.Node { + t.Helper() + fields := mappingFields(t, name, parent) + if len(fields) != 1 || fields[key] == nil { + t.Fatalf("%s must contain only %s", name, key) + } + return mappingFields(t, name, fields[key]) +} + +func mappingFields(t *testing.T, name string, node *yaml.Node) map[string]*yaml.Node { + t.Helper() + if node.Kind != yaml.MappingNode { + t.Fatalf("%s must contain a mapping", name) + } + fields := make(map[string]*yaml.Node, len(node.Content)/2) + for index := 0; index+1 < len(node.Content); index += 2 { + key := node.Content[index].Value + if _, exists := fields[key]; exists { + t.Fatalf("%s repeats key %q", name, key) + } + fields[key] = node.Content[index+1] } return fields } - -func assertNoSecretKeys(t *testing.T, path string, node *yaml.Node) { - t.Helper() - - forbidden := map[string]bool{ - "api_key": true, - "apikey": true, - "authorization": true, - "credential": true, - "credentials": true, - "password": true, - "secret": true, - "token": true, - "access_token": true, - } - var visit func(*yaml.Node) - visit = func(current *yaml.Node) { - if current.Kind == yaml.MappingNode { - for index := 0; index+1 < len(current.Content); index += 2 { - if forbidden[strings.ToLower(current.Content[index].Value)] { - t.Fatalf("profile %s contains a prohibited extra_params key", path) - } - visit(current.Content[index+1]) - } - return - } - for _, child := range current.Content { - visit(child) - } - } - visit(node) -} diff --git a/docs/release.md b/docs/release.md index 6d2fd74..632c869 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,11 +1,11 @@ # Catalog Release Procedure This repository releases source tags only. It has no generated artifacts. The -module path is `gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2`. +module path is `gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3`. ## Version selection -Release versions use `v2.YYYYMMDD.N`. `YYYYMMDD` is the actual UTC publication +Release versions use `v3.YYYYMMDD.N`. `YYYYMMDD` is the actual UTC publication date. Begin at revision `0` for a date and increment `N` for each additional release on that date. Versions are immutable: correct a published catalog with a new dated version rather than moving or recreating a tag. This repository @@ -20,7 +20,7 @@ tree is active, then run `GOWORK=off go test ./...`, `GOWORK=off go test -race ./...`, `GOWORK=off go vet ./...`, `GOWORK=off go build ./...`, formatting, repository-hygiene, credential, and ordinary module-resolution checks. -Confirm the module reports the `/v2` identity before publication. Create an +Confirm the module reports the `/v3` identity before publication. Create an annotated tag only after validation succeeds. Do not create or move an existing tag. Publish the validated source commit through `main`, then push only the selected annotated tag and verify that the remote tag and its peeled commit @@ -28,5 +28,5 @@ identify that source commit. Finally, from a temporary module with `GOWORK=off`, no workspace, no replacement, and no vendor tree, resolve the exact published -`gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2@version`. Verify +`gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3@version`. Verify both the resolved module identity and version match the selected tag. diff --git a/go.mod b/go.mod index 0681d0a..0e1f2bd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 +module gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3 go 1.25.5