Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ from `AGENTS.md`.
| [Modern agentic AI primer](engineering/intro_to_modern_agentic_ai_for_coding_agents.md) | Active principle/background guide | Routed background for substantial agent-behaviour planning; not a current-status report |
| [VWL manual](engineering/von_workflow_language_manual.md) | Active manual | Canonical reference for VWL vocabulary and runtime interfaces; consult relevant sections, and verify live artefacts/behaviour |
| [Prompt programmes and model routing](engineering/prompt_programs_and_model_routing_playbook.md) | Active playbook | Normative only within its stated prompt/model scope |
| [OpenRouter provider deployment](engineering/openrouter_provider_deployment.md) | Active bounded operator note | Operational reference for the opt-in OpenRouter transport; scoped settings and represented model policy remain authoritative |
| [Agent memory and enduring knowledge](engineering/agent_memory_and_enduring_knowledge.md) | Active design guide | Normative only within its stated memory/retrieval scope |
| [Contextual knowledge evolution](engineering/contextual_knowledge_evolution.md) | Active design guide | Advisory under `AGENTS.md` when assertion/context distinctions are material; it does not select a microtheory formalism or prove present implementation |
| [Ontology publication authority](engineering/ontology_publication_authority.md) | Active design/implementation boundary | Operational reference for `JVNAUTOSCI-2632` and represented publication-profile semantics from `JVNAUTOSCI-2671`; distinguish semantic advice and publication from visibility and operational administration, and revalidate the current runtime, profiles, and represented roles before relying on deployment-specific claims |
Expand Down
49 changes: 49 additions & 0 deletions docs/engineering/openrouter_provider_deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# OpenRouter Provider Deployment Note

- **Kind:** Bounded deployment and operator note
- **Lifecycle:** Active
- **Authority:** Operational guidance for the opt-in OpenRouter transport only;
model eligibility and routing remain in scoped settings and the represented
model registry/workflows
- **Last reviewed:** 27 August 2026
- **Review trigger:** OpenRouter API, privacy, pricing, or Von provider-policy changes

OpenRouter is an optional external inference source. It broadens the catalogue
available for evaluation, but configuring a key does not enable a model, alter
the scoped primary, or make OpenRouter a default. An authenticated actor must
still have the exact `openrouter` provider and model slug in their effective
enabled-model setting.

Configure `OPENROUTER_API_KEY` or, preferably on managed hosts,
`OPENROUTER_API_KEY_FILE`. The OpenStack bootstrap variables are
`bootstrap_openrouter_api_key` and `bootstrap_openrouter_api_key_file`; keep the
inline value outside tracked tfvars. Von uses
`https://openrouter.ai/api/v1` unless `OPENROUTER_BASE_URL` explicitly selects
another compatible endpoint.

Every Von OpenRouter generation in this release uses Chat Completions and
requires the following provider preferences:

- `zdr: true`;
- `data_collection: "deny"`; and
- `require_parameters: true`.

Router metadata is requested so telemetry can retain the requested provider and
model, concrete response model, generation identifier, upstream routing
evidence, token usage, and provider-reported cost when OpenRouter supplies it.
Missing cost or routing data remains unavailable rather than being recorded as
zero. The catalogue is discovery data only and does not grant model authority.

OpenRouter charges and upstream rate limits vary by model and provider. Check
the current OpenRouter model catalogue and account limits before allowing a
model. Zero-data-retention eligibility and denial of data-collection endpoints
reduce retention exposure but do not make an external broker equivalent to a
local model; prompts still cross Von's trust boundary and request metadata may
be retained. This is why OpenRouter remains explicit and opt-in for private or
organisation-sensitive work.

This release does not enable OpenRouter Auto Router, cross-model fallback,
Responses API, embeddings, response caching, server tools/plugins/guardrails,
or deployment activation. Same-model upstream selection remains observable
OpenRouter transport behaviour; Von's represented policy still chooses the
model.
4 changes: 4 additions & 0 deletions infra/openstack/environments/dev/dev.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ bootstrap_openai_api_key_file = "/etc/von/secrets/openai_api_key"
# Leave bootstrap_gemini_api_key unset when using TF_VAR_bootstrap_gemini_api_key.
# bootstrap_gemini_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_gemini_api_key_file = "/etc/von/secrets/gemini_api_key"
# Leave bootstrap_openrouter_api_key unset when using TF_VAR_bootstrap_openrouter_api_key.
# OpenRouter remains opt-in; do not make it the default without an explicit scoped model.
# bootstrap_openrouter_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_openrouter_api_key_file = "/etc/von/secrets/openrouter_api_key"

# Optional: seed a scoped model setting after first deploy.
# Set all three values together to seed the organisation default.
Expand Down
4 changes: 4 additions & 0 deletions infra/openstack/environments/prod/prod.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ bootstrap_openai_api_key_file = "/etc/von/secrets/openai_api_key"
# Leave bootstrap_gemini_api_key unset when using TF_VAR_bootstrap_gemini_api_key.
# bootstrap_gemini_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_gemini_api_key_file = "/etc/von/secrets/gemini_api_key"
# Leave bootstrap_openrouter_api_key unset when using TF_VAR_bootstrap_openrouter_api_key.
# OpenRouter remains opt-in; do not make it the default without an explicit scoped model.
# bootstrap_openrouter_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_openrouter_api_key_file = "/etc/von/secrets/openrouter_api_key"

# Optional: seed a scoped model setting after first deploy.
# Set all three values together to seed the organisation default.
Expand Down
4 changes: 4 additions & 0 deletions infra/openstack/environments/staging/staging.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ bootstrap_openai_api_key_file = "/etc/von/secrets/openai_api_key"
# Leave bootstrap_gemini_api_key unset when using TF_VAR_bootstrap_gemini_api_key.
# bootstrap_gemini_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_gemini_api_key_file = "/etc/von/secrets/gemini_api_key"
# Leave bootstrap_openrouter_api_key unset when using TF_VAR_bootstrap_openrouter_api_key.
# OpenRouter remains opt-in; do not make it the default without an explicit scoped model.
# bootstrap_openrouter_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_openrouter_api_key_file = "/etc/von/secrets/openrouter_api_key"

# Optional: seed a scoped model setting after first deploy.
# Set all three values together to seed the organisation default.
Expand Down
2 changes: 2 additions & 0 deletions infra/openstack/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ locals {
openai_api_key_file = var.bootstrap_openai_api_key_file
gemini_api_key = var.bootstrap_gemini_api_key
gemini_api_key_file = var.bootstrap_gemini_api_key_file
openrouter_api_key = var.bootstrap_openrouter_api_key
openrouter_api_key_file = var.bootstrap_openrouter_api_key_file
default_llm_provider = var.bootstrap_default_llm_provider
default_llm_model = var.bootstrap_default_llm_model
default_llm_organisation_concept_id = var.bootstrap_default_llm_organisation_concept_id
Expand Down
11 changes: 10 additions & 1 deletion infra/openstack/templates/cloud-init/von_bootstrap.yaml.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ write_files:
GOOGLE_OAUTH_ENABLE_DYNAMIC_REDIRECTS=${google_oauth_enable_dynamic_redirects ? "true" : "false"}
OPENAI_API_KEY_FILE=${openai_api_key_file}
GEMINI_API_KEY_FILE=${gemini_api_key_file}
OPENROUTER_API_KEY_FILE=${openrouter_api_key_file}
%{ if default_llm_provider != null && lower(default_llm_provider) == "openai" && default_llm_model != null ~}
VON_DEFAULT_OPENAI_MODEL=${jsonencode(default_llm_model)}
%{ endif ~}
Expand Down Expand Up @@ -98,6 +99,14 @@ write_files:
${gemini_api_key}
%{ endif ~}

%{ if openrouter_api_key != null ~}
- path: ${openrouter_api_key_file}
owner: root:root
permissions: "0600"
content: |
${openrouter_api_key}
%{ endif ~}

%{ if mongo_uri != null ~}
- path: ${mongo_uri_file}
owner: root:root
Expand Down Expand Up @@ -207,7 +216,7 @@ runcmd:
- [bash, -lc, "set -euo pipefail; install -d -m 0755 -o root -g root '${backup_directory}' '${central_log_directory}'"]
- [bash, -lc, "set -euo pipefail; install -d -m 0755 -o root -g root /etc/von"]
- [bash, -lc, "set -euo pipefail; install -d -m 0750 -o root -g ${service_group} /etc/von/secrets"]
- [bash, -lc, "set -euo pipefail; for secret_file in '${google_oauth_client_id_file}' '${google_oauth_client_secret_file}' '${openai_api_key_file}' '${gemini_api_key_file}' '${mongo_uri_file}'; do if [ -f \"$secret_file\" ]; then chown root:${service_group} \"$secret_file\"; chmod 0640 \"$secret_file\"; fi; done"]
- [bash, -lc, "set -euo pipefail; for secret_file in '${google_oauth_client_id_file}' '${google_oauth_client_secret_file}' '${openai_api_key_file}' '${gemini_api_key_file}' '${openrouter_api_key_file}' '${mongo_uri_file}'; do if [ -f \"$secret_file\" ]; then chown root:${service_group} \"$secret_file\"; chmod 0640 \"$secret_file\"; fi; done"]
%{ if enable_https && generate_self_signed_cert ~}
- [bash, -lc, "set -euo pipefail; if [ ! -f '${tls_cert_path}' ] || [ ! -f '${tls_key_path}' ]; then install -d -m 0755 /etc/ssl/certs /etc/ssl/private; openssl req -x509 -nodes -newkey rsa:4096 -days 365 -subj '/CN=${domain_name}' -keyout '${tls_key_path}' -out '${tls_cert_path}'; chmod 0600 '${tls_key_path}'; fi"]
%{ endif ~}
Expand Down
27 changes: 25 additions & 2 deletions infra/openstack/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -562,14 +562,37 @@ variable "bootstrap_gemini_api_key_file" {
}
}

variable "bootstrap_openrouter_api_key" {
type = string
description = "Optional inline OpenRouter API key for managed bootstrap secret-file injection."
default = null
sensitive = true

validation {
condition = var.bootstrap_openrouter_api_key == null || trimspace(var.bootstrap_openrouter_api_key) != ""
error_message = "bootstrap_openrouter_api_key must be null or non-empty."
}
}

variable "bootstrap_openrouter_api_key_file" {
type = string
description = "Path used by runtime OPENROUTER_API_KEY_FILE."
default = "/etc/von/secrets/openrouter_api_key"

validation {
condition = can(regex("^/", var.bootstrap_openrouter_api_key_file))
error_message = "bootstrap_openrouter_api_key_file must be an absolute Linux path."
}
}

variable "bootstrap_default_llm_provider" {
type = string
description = "Optional provider to seed as the scoped cloud default LLM setting."
default = null

validation {
condition = var.bootstrap_default_llm_provider == null || contains(["openai", "gemini", "ollama"], lower(trimspace(var.bootstrap_default_llm_provider)))
error_message = "bootstrap_default_llm_provider must be null, openai, gemini, or ollama."
condition = var.bootstrap_default_llm_provider == null || contains(["openai", "openrouter", "gemini", "ollama"], lower(trimspace(var.bootstrap_default_llm_provider)))
error_message = "bootstrap_default_llm_provider must be null, openai, openrouter, gemini, or ollama."
}
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/bootstrap_llm_setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pathlib import Path


SUPPORTED_PROVIDERS = {"openai", "gemini", "ollama"}
SUPPORTED_PROVIDERS = {"openai", "openrouter", "gemini", "ollama"}


def _normalise_concept_id(raw: str | None) -> str | None:
Expand Down
9 changes: 8 additions & 1 deletion scripts/replay_llm_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,10 @@ def _client_for_provider(
from src.backend.languagemodels.llm_interface import OpenAIClient

return OpenAIClient()
if provider_key == "openrouter":
from src.backend.languagemodels.llm_interface import OpenRouterClient

return OpenRouterClient()
if provider_key == "gemini":
from src.backend.languagemodels.llm_interface import GeminiClient

Expand Down Expand Up @@ -744,7 +748,10 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--call-type", help="Filter by call type.")
parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
parser.add_argument("--inspect-only", action="store_true")
parser.add_argument("--provider", choices=["ollama", "openai", "gemini", "mock"])
parser.add_argument(
"--provider",
choices=["ollama", "openai", "openrouter", "gemini", "mock"],
)
parser.add_argument("--model")
parser.add_argument("--timeout", type=float, default=120.0)
parser.add_argument("--repeat", type=int, default=1)
Expand Down
14 changes: 8 additions & 6 deletions src/backend/integrations/internal_mcp/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ def _provider_from_llm_client(llm_client: Any) -> str | None:
class_name = type(llm_client).__name__.lower()
if "ollama" in class_name:
return "ollama"
if "openrouter" in class_name:
return "openrouter"
if "openai" in class_name:
return "openai"
if "gemini" in class_name:
Expand All @@ -381,7 +383,7 @@ def _model_identifier_looks_local_ollama(model: Any) -> bool:
return False
if cleaned.startswith("ollama:"):
return True
if cleaned.startswith(("openai:", "anthropic:", "gemini:", "azure_openai:")):
if cleaned.startswith(("openai:", "openrouter:", "anthropic:", "gemini:", "azure_openai:")):
return False
if cleaned.startswith(("gpt-", "o1-", "claude", "gemini")):
return False
Expand Down Expand Up @@ -13582,7 +13584,7 @@ def _normalise_llm_model_name(model: Optional[str]) -> Optional[str]:

candidate_tail = candidate
lowered_candidate = candidate.lower()
for provider_prefix in ("openai:", "ollama:", "gemini:"):
for provider_prefix in ("openai:", "openrouter:", "ollama:", "gemini:"):
if lowered_candidate.startswith(provider_prefix):
candidate_tail = candidate.split(":", 1)[1].strip()
break
Expand Down Expand Up @@ -13694,7 +13696,7 @@ def _parse_policy_model_candidate(value: Any) -> _ModelCandidate | None:
elif ":" in candidate:
provider_prefix, _, model_suffix = candidate.partition(":")
provider_prefix = provider_prefix.strip().lower()
if provider_prefix in {"openai", "ollama", "gemini"}:
if provider_prefix in {"openai", "openrouter", "ollama", "gemini"}:
provider = provider_prefix
model = model_suffix.strip()
elif candidate.lower().startswith("ft:"):
Expand Down Expand Up @@ -13950,7 +13952,7 @@ def _infer_provider_from_model_reference(model_ref: str | None) -> str | None:
if ":" in raw and not raw.startswith("#V#"):
provider, _, _model = raw.partition(":")
provider_text = provider.strip().lower()
if provider_text in {"openai", "ollama", "gemini"}:
if provider_text in {"openai", "openrouter", "ollama", "gemini"}:
return provider_text
if raw.lower().startswith("ft:"):
return "openai"
Expand Down Expand Up @@ -14984,7 +14986,7 @@ def _create_client_for_candidate(
if (
model_implied_provider
and model_implied_provider != default_provider
and model_implied_provider in {"openai", "ollama", "gemini"}
and model_implied_provider in {"openai", "openrouter", "ollama", "gemini"}
):
requested_provider_client = model_implied_provider
client = get_llm_client(
Expand Down Expand Up @@ -15079,7 +15081,7 @@ def _create_client_for_candidate(
if model:
telemetry["model"] = model

if provider in {None, "", "openai", "ollama", "gemini"}:
if provider in {None, "", "openai", "openrouter", "ollama", "gemini"}:
pass
else:
telemetry["error"] = "unsupported_provider"
Expand Down
Loading
Loading