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
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ MONGO_ALLOW_LOCAL_FALLBACK=0
# Legacy backup aliases: GOOGLE_API_BACKUP_KEY and GOOGLE_API_BACKUP_KEY_FILE
# VON_DEFAULT_GEMINI_MODEL=gemini-3.7-flash

# Meta Muse API Key
# Canonical direct value; managed hosts may use META_API_KEY_FILE instead.
# META_API_KEY=<YOUR-META-KEY-HERE>
# META_API_KEY_FILE=/etc/von/secrets/meta_api_key

# ============================================================================
# WEB SEARCH (Optional)
# ============================================================================
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ architecture:
- a browser application centred on persistent, multi-turn conversations, with
inspectable shared situations, uploads, tasks, tool progress, and workflow
state;
- model support for local Ollama and configured OpenAI, Gemini, and opt-in
OpenRouter providers;
- model support for local Ollama, configured OpenAI, and actor-scoped, opt-in
Gemini, Meta Muse, and OpenRouter providers;
- an internal Model Context Protocol (MCP) gateway and capability catalogue spanning represented
knowledge, retrieval, scholarly sources, web search, tasks, workflows, and
configured external services;
Expand Down Expand Up @@ -190,8 +190,10 @@ Remote database failures do not silently fall back to a possibly stale local
database unless that fallback is explicitly enabled.

For local models, install and run Ollama and configure `OLLAMA_HOSTS_LIST`. For
hosted providers, set the applicable credential, such as `OPENAI_API_KEY` or
`GEMINI_API_KEY`, then select an eligible provider and model in Von's settings.
hosted providers, set the applicable credential, such as `OPENAI_API_KEY`,
`GEMINI_API_KEY`, or `META_API_KEY`, then select an eligible provider and model
in Von's settings. Meta Muse supports the fixed model `muse-spark-1.3`; managed
hosts may supply its key through `META_API_KEY_FILE`.
You may also configure `OPENAI_API_BACKUP_KEY` or `GEMINI_API_BACKUP_KEY` (or
their `*_FILE` forms). Von uses a backup once only after the primary credential
is rejected for authentication, quota, or rate limiting, and marks the model
Expand Down
10 changes: 10 additions & 0 deletions docs/engineering/environment_minimums.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ $env:OPENAI_API_KEY = '<YOUR-OPENAI-KEY>'
# or
$env:GEMINI_API_KEY = '<YOUR-GEMINI-KEY>'
# or
$env:META_API_KEY = '<YOUR-META-KEY>'
# or
$env:OLLAMA_HOST = 'http://<host>:11434'
```

Expand All @@ -54,6 +56,14 @@ For legacy Gemini naming, `GOOGLE_API_BACKUP_KEY` and
`GOOGLE_API_BACKUP_KEY_FILE` are accepted as aliases. New setups should use
the `GEMINI_API_BACKUP_KEY` names.

Meta Muse uses only the canonical `META_API_KEY` credential name. Managed
deployments can instead set
`META_API_KEY_FILE=/etc/von/secrets/meta_api_key`; Von reads the secret from
that file without exposing it through Settings. The supported Meta model is
`muse-spark-1.3`. Catalogue verification does not allow or select the model:
the exact `meta:muse-spark-1.3` pair must first be saved in the actor-scoped
enabled model pool before a paid model probe or generation request.

## 2. How Von decides whether MongoDB is local or remote

Von does not have a separate `local/cloud` switch for MongoDB. It inspects the
Expand Down
4 changes: 4 additions & 0 deletions infra/openstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ needed for real chat/workflow execution:
- `bootstrap_gemini_api_key_file` defaults to
`/etc/von/secrets/gemini_api_key`. Gemini keys can be injected with
`TF_VAR_bootstrap_gemini_api_key` or pre-provisioned at that path.
- `bootstrap_meta_api_key_file` defaults to `/etc/von/secrets/meta_api_key`.
Meta Model API keys can be injected with `TF_VAR_bootstrap_meta_api_key` or
pre-provisioned at that path.
- `bootstrap_default_llm_provider`, `bootstrap_default_llm_model`, and either
`bootstrap_default_llm_organisation_concept_id` or
`bootstrap_default_llm_user_concept_id` can seed the scoped LLM setting after
Expand All @@ -200,6 +203,7 @@ needed for real chat/workflow execution:
Do not assign `bootstrap_openai_api_key = null` in local tfvars when using
`TF_VAR_bootstrap_openai_api_key`; tfvars values override runtime environment
variables. The same rule applies to `bootstrap_gemini_api_key`.
It also applies to `bootstrap_meta_api_key`.

Operational note:

Expand Down
5 changes: 5 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_meta_api_key unset when using TF_VAR_bootstrap_meta_api_key.
# Meta Muse remains opt-in; do not make it the default without an explicit scoped model.
# bootstrap_meta_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_meta_api_key_file = "/etc/von/secrets/meta_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"
Expand All @@ -105,6 +109,7 @@ bootstrap_openrouter_api_key_file = "/etc/von/secrets/openrouter_api_key"
# bootstrap_default_llm_provider = "openai"
# bootstrap_default_llm_model = "gpt-5.5"
# Gemini alternative: provider = "gemini", model = "gemini-3.7-flash".
# Meta Muse alternative: provider = "meta", model = "muse-spark-1.3".
# bootstrap_default_llm_organisation_concept_id = "#V#university_of_auckland_strong_ai_lab"

# Mongo Atlas hardening (dev can remain non-strict while preserving TLS defaults).
Expand Down
5 changes: 5 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_meta_api_key unset when using TF_VAR_bootstrap_meta_api_key.
# Meta Muse remains opt-in; do not make it the default without an explicit scoped model.
# bootstrap_meta_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_meta_api_key_file = "/etc/von/secrets/meta_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"
Expand All @@ -109,6 +113,7 @@ bootstrap_openrouter_api_key_file = "/etc/von/secrets/openrouter_api_key"
# bootstrap_default_llm_provider = "openai"
# bootstrap_default_llm_model = "gpt-5.5"
# Gemini alternative: provider = "gemini", model = "gemini-3.7-flash".
# Meta Muse alternative: provider = "meta", model = "muse-spark-1.3".
# bootstrap_default_llm_organisation_concept_id = "#V#university_of_auckland_strong_ai_lab"

# Mongo Atlas hardening for hosted production.
Expand Down
5 changes: 5 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_meta_api_key unset when using TF_VAR_bootstrap_meta_api_key.
# Meta Muse remains opt-in; do not make it the default without an explicit scoped model.
# bootstrap_meta_api_key = "REPLACE_WITH_LOCAL_UNTRACKED_SECRET"
bootstrap_meta_api_key_file = "/etc/von/secrets/meta_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"
Expand All @@ -101,6 +105,7 @@ bootstrap_openrouter_api_key_file = "/etc/von/secrets/openrouter_api_key"
# bootstrap_default_llm_provider = "openai"
# bootstrap_default_llm_model = "gpt-5.5"
# Gemini alternative: provider = "gemini", model = "gemini-3.7-flash".
# Meta Muse alternative: provider = "meta", model = "muse-spark-1.3".
# bootstrap_default_llm_organisation_concept_id = "#V#university_of_auckland_strong_ai_lab"

# Mongo Atlas hardening for hosted staging.
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
meta_api_key = var.bootstrap_meta_api_key
meta_api_key_file = var.bootstrap_meta_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
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}
META_API_KEY_FILE=${meta_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)}
Expand Down Expand Up @@ -99,6 +100,14 @@ write_files:
${gemini_api_key}
%{ endif ~}

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

%{ if openrouter_api_key != null ~}
- path: ${openrouter_api_key_file}
owner: root:root
Expand Down Expand Up @@ -216,7 +225,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}' '${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"]
- [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}' '${meta_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,6 +562,29 @@ variable "bootstrap_gemini_api_key_file" {
}
}

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

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

variable "bootstrap_meta_api_key_file" {
type = string
description = "Path used by runtime META_API_KEY_FILE."
default = "/etc/von/secrets/meta_api_key"

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

variable "bootstrap_openrouter_api_key" {
type = string
description = "Optional inline OpenRouter API key for managed bootstrap secret-file injection."
Expand Down Expand Up @@ -591,8 +614,8 @@ variable "bootstrap_default_llm_provider" {
default = null

validation {
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."
condition = var.bootstrap_default_llm_provider == null || contains(["openai", "openrouter", "gemini", "meta", "ollama"], lower(trimspace(var.bootstrap_default_llm_provider)))
error_message = "bootstrap_default_llm_provider must be null, openai, openrouter, gemini, meta, 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", "openrouter", "gemini", "ollama"}
SUPPORTED_PROVIDERS = {"openai", "openrouter", "gemini", "meta", "ollama"}


def _normalise_concept_id(raw: str | None) -> str | None:
Expand Down
1 change: 1 addition & 0 deletions src/backend/integrations/internal_mcp/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -34321,6 +34321,7 @@ def _sanitise_for_introspection(value: Any, *, depth: int = 0) -> Any:
"ANTHROPIC_API_KEY",
"GEMINI_API_KEY",
"GEMINI_API_BACKUP_KEY",
"META_API_KEY",
"GOOGLE_API_KEY",
"GOOGLE_API_BACKUP_KEY",
"MISTRAL_API_KEY",
Expand Down
62 changes: 50 additions & 12 deletions src/backend/integrations/internal_mcp/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ def _provider_from_llm_client(llm_client: Any) -> str | None:
return "openai"
if "gemini" in class_name:
return "gemini"
if "metamuse" in class_name or "meta_muse" in class_name:
return "meta"
if "anthropic" in class_name or "claude" in class_name:
return "anthropic"
return None
Expand All @@ -385,7 +387,14 @@ def _model_identifier_looks_local_ollama(model: Any) -> bool:
if cleaned.startswith("ollama:"):
return True
if cleaned.startswith(
("openai:", "openrouter:", "anthropic:", "gemini:", "azure_openai:")
(
"openai:",
"openrouter:",
"anthropic:",
"gemini:",
"meta:",
"azure_openai:",
)
):
return False
if cleaned.startswith(("gpt-", "o1-", "claude", "gemini")):
Expand Down Expand Up @@ -13654,7 +13663,13 @@ def _normalise_llm_model_name(model: Optional[str]) -> Optional[str]:

candidate_tail = candidate
lowered_candidate = candidate.lower()
for provider_prefix in ("openai:", "openrouter:", "ollama:", "gemini:"):
for provider_prefix in (
"openai:",
"openrouter:",
"ollama:",
"gemini:",
"meta:",
):
if lowered_candidate.startswith(provider_prefix):
candidate_tail = candidate.split(":", 1)[1].strip()
break
Expand Down Expand Up @@ -13766,7 +13781,13 @@ 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", "openrouter", "ollama", "gemini"}:
if provider_prefix in {
"openai",
"openrouter",
"ollama",
"gemini",
"meta",
}:
provider = provider_prefix
model = model_suffix.strip()
elif candidate.lower().startswith("ft:"):
Expand All @@ -13792,6 +13813,7 @@ def _parse_policy_model_candidate(value: Any) -> _ModelCandidate | None:
if model == "default":
from src.backend.languagemodels.model_defaults import (
DEFAULT_GEMINI_MODEL,
DEFAULT_META_MUSE_MODEL,
DEFAULT_OLLAMA_MODEL,
DEFAULT_OPENAI_MODEL,
)
Expand All @@ -13800,6 +13822,7 @@ def _parse_policy_model_candidate(value: Any) -> _ModelCandidate | None:
"ollama": DEFAULT_OLLAMA_MODEL,
"openai": DEFAULT_OPENAI_MODEL,
"gemini": DEFAULT_GEMINI_MODEL,
"meta": DEFAULT_META_MUSE_MODEL,
}
resolved = _provider_defaults.get(provider)
if resolved:
Expand Down Expand Up @@ -14022,7 +14045,13 @@ 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", "openrouter", "ollama", "gemini"}:
if provider_text in {
"openai",
"openrouter",
"ollama",
"gemini",
"meta",
}:
return provider_text
if raw.lower().startswith("ft:"):
return "openai"
Expand Down Expand Up @@ -15057,7 +15086,7 @@ def _create_client_for_candidate(
model_implied_provider
and model_implied_provider != default_provider
and model_implied_provider
in {"openai", "openrouter", "ollama", "gemini"}
in {"openai", "openrouter", "ollama", "gemini", "meta"}
):
requested_provider_client = model_implied_provider
client = get_llm_client(
Expand Down Expand Up @@ -15091,10 +15120,11 @@ def _create_client_for_candidate(
resolved_model = resolve_ollama_model_name(resolved_model)
resolved_model = self._normalise_llm_model_name(resolved_model)
except Exception as exc:
if requested_provider_client == "gemini":
if requested_provider_client in {"gemini", "meta"}:
failed_provider = requested_provider_client
telemetry.update(
{
"provider": "gemini",
"provider": failed_provider,
"model": default_model,
"error": "candidate_client_resolution_failed",
"error_class": type(exc).__name__,
Expand All @@ -15104,7 +15134,7 @@ def _create_client_for_candidate(
}
)
raise _ModelCandidateClientResolutionError(
provider="gemini",
provider=failed_provider,
model=default_model,
telemetry=telemetry,
cause_class=type(exc).__name__,
Expand Down Expand Up @@ -15152,7 +15182,15 @@ def _create_client_for_candidate(
if model:
telemetry["model"] = model

if provider in {None, "", "openai", "openrouter", "ollama", "gemini"}:
if provider in {
None,
"",
"openai",
"openrouter",
"ollama",
"gemini",
"meta",
}:
pass
else:
telemetry["error"] = "unsupported_provider"
Expand Down Expand Up @@ -15198,10 +15236,10 @@ def _create_client_for_candidate(
telemetry["model_resolution_source"] = "client_default"
return client, model or default_model, telemetry
except Exception as exc:
if provider == "gemini":
if provider in {"gemini", "meta"}:
telemetry.update(
{
"provider": "gemini",
"provider": provider,
"model": model,
"error": "candidate_client_resolution_failed",
"error_class": type(exc).__name__,
Expand All @@ -15211,7 +15249,7 @@ def _create_client_for_candidate(
}
)
raise _ModelCandidateClientResolutionError(
provider="gemini",
provider=provider,
model=model,
telemetry=telemetry,
cause_class=type(exc).__name__,
Expand Down
Loading
Loading