-
Notifications
You must be signed in to change notification settings - Fork 0
fix: replace Provider HTTP module with beam_agent session-based model listing #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
06e54e1
e378d77
2097dde
f14c95c
83bf4c2
e391a62
a03950f
96af381
2514c07
dc872e5
4f2456e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,8 @@ defmodule MonkeyClaw.ModelRegistry do | |
| alias MonkeyClaw.ModelRegistry.EtsHeir | ||
| alias MonkeyClaw.ModelRegistry.Provider | ||
| alias MonkeyClaw.Repo | ||
| alias MonkeyClaw.Vault | ||
| alias MonkeyClaw.Workspaces | ||
|
|
||
| @ets_table :monkey_claw_model_registry | ||
| @default_interval_ms :timer.hours(24) | ||
|
|
@@ -160,6 +162,26 @@ defmodule MonkeyClaw.ModelRegistry do | |
| GenServer.call(__MODULE__, :refresh_all, :infinity) | ||
| end | ||
|
|
||
| @doc """ | ||
| Probe upstream APIs using vault secrets from the given workspace. | ||
|
|
||
| Discovers which vault secrets map to known backends via a static | ||
| `provider → backend` mapping, then probes each backend synchronously | ||
| with the corresponding secret name and workspace ID. On success, | ||
| auto-configures `state.backends`, `state.backend_configs`, and | ||
| `state.workspace_id` so future tick probes continue without manual | ||
| intervention. | ||
|
|
||
| Returns `:ok` when at least one backend was probed (individual probe | ||
| failures are logged but do not fail the call), or | ||
| `{:error, :no_backends_discovered}` when no vault secrets map to a | ||
| known backend. | ||
| """ | ||
| @spec refresh_for_workspace(Ecto.UUID.t()) :: :ok | {:error, :no_backends_discovered} | ||
| def refresh_for_workspace(workspace_id) when is_binary(workspace_id) do | ||
| GenServer.call(__MODULE__, {:refresh_for_workspace, workspace_id}, :infinity) | ||
| end | ||
|
|
||
| @doc """ | ||
| Register a capability token for session-hook writes. | ||
|
|
||
|
|
@@ -314,6 +336,30 @@ defmodule MonkeyClaw.ModelRegistry do | |
| {:reply, :ok, state} | ||
| end | ||
|
|
||
| def handle_call({:refresh_for_workspace, workspace_id}, _from, %State{} = state) do | ||
| case discover_workspace_backends(workspace_id) do | ||
| [] -> | ||
| {:reply, {:error, :no_backends_discovered}, state} | ||
|
|
||
| backend_specs -> | ||
| state = bootstrap_workspace_config(backend_specs, workspace_id, state) | ||
|
|
||
| state = | ||
| Enum.reduce(backend_specs, state, fn {backend, secret_name}, acc -> | ||
| {_result, new_state} = | ||
| do_synchronous_probe( | ||
| backend, | ||
| probe_opts_for_workspace(backend, workspace_id, secret_name), | ||
| acc | ||
| ) | ||
|
|
||
| new_state | ||
| end) | ||
|
|
||
| {:reply, :ok, state} | ||
| end | ||
| end | ||
|
|
||
| def handle_call({:configure, opts}, _from, %State{} = state) do | ||
| case validate_configure_opts(opts, state) do | ||
| :ok -> | ||
|
|
@@ -358,6 +404,7 @@ defmodule MonkeyClaw.ModelRegistry do | |
| @impl true | ||
| def handle_info(:tick, %State{} = state) do | ||
| state = maybe_retry_sqlite_load(state) | ||
| state = maybe_auto_discover(state) | ||
| state = Enum.reduce(state.backends, state, &maybe_dispatch_probe/2) | ||
| state = schedule_next_tick(state) | ||
| {:noreply, state} | ||
|
Comment on lines
411
to
416
|
||
|
|
@@ -524,8 +571,10 @@ defmodule MonkeyClaw.ModelRegistry do | |
| # ── Private — Synchronous probe ───────────────────────────── | ||
|
|
||
| defp do_synchronous_probe(backend, state) do | ||
| {adapter, opts} = probe_opts(backend, state) | ||
| do_synchronous_probe(backend, probe_opts(backend, state), state) | ||
| end | ||
|
|
||
| defp do_synchronous_probe(backend, {adapter, opts}, state) do | ||
| task = | ||
| Task.Supervisor.async_nolink(MonkeyClaw.TaskSupervisor, fn -> | ||
| adapter.list_models(opts) | ||
|
|
@@ -561,6 +610,104 @@ defmodule MonkeyClaw.ModelRegistry do | |
| end | ||
| end | ||
|
|
||
| # ── Private — Workspace-aware probing ────────────────────── | ||
|
|
||
| # Static reverse mapping from vault secret provider to ModelRegistry | ||
| # backend name. Mirrors the forward mapping in BeamAgent.backend_to_provider/1. | ||
| @provider_to_backend %{ | ||
| "anthropic" => "claude", | ||
| "openai" => "codex", | ||
| "google" => "gemini", | ||
| "github_copilot" => "copilot" | ||
|
beardedeagle marked this conversation as resolved.
|
||
| } | ||
|
beardedeagle marked this conversation as resolved.
|
||
|
|
||
| # Discovers which vault secrets in a workspace map to known backends. | ||
| # Returns [{backend, secret_name}] — one entry per discoverable backend. | ||
| @spec discover_workspace_backends(Ecto.UUID.t()) :: [{String.t(), String.t()}] | ||
| defp discover_workspace_backends(workspace_id) do | ||
| workspace_id | ||
| |> Vault.list_secrets() | ||
| |> Enum.flat_map(fn secret -> | ||
| case Map.get(@provider_to_backend, secret.provider) do | ||
| nil -> [] | ||
| backend -> [{backend, secret.name}] | ||
| end | ||
| end) | ||
| |> Enum.uniq_by(fn {backend, _} -> backend end) | ||
| end | ||
|
|
||
| # Builds {adapter, opts} for a workspace-derived probe — bypasses | ||
| # state.backend_configs and injects workspace_id + secret_name directly. | ||
| defp probe_opts_for_workspace(backend, workspace_id, secret_name) do | ||
| {MonkeyClaw.AgentBridge.Backend.BeamAgent, | ||
| %{backend: backend, workspace_id: workspace_id, secret_name: secret_name}} | ||
| end | ||
|
|
||
| # Merges discovered backends into GenServer state so future tick | ||
| # probes and refresh_all/0 calls work without reconfiguration. | ||
| # When no backends are configured, discover them from the first | ||
| # workspace's vault secrets. Single-user, single-instance: the first | ||
| # workspace is the only workspace. Fires once per tick when backends | ||
| # is empty — if no workspace or secrets exist yet, returns state | ||
| # unchanged and retries on the next tick. | ||
| defp maybe_auto_discover(%State{backends: []} = state) do | ||
| case auto_discover_backends() do | ||
| {[_ | _] = specs, workspace_id} -> | ||
| Logger.info( | ||
| "ModelRegistry: auto-discovered #{length(specs)} backend(s) from workspace vault" | ||
| ) | ||
|
|
||
| bootstrap_workspace_config(specs, workspace_id, state) | ||
|
|
||
| _ -> | ||
| state | ||
| end | ||
| end | ||
|
|
||
| defp maybe_auto_discover(state), do: state | ||
|
|
||
| defp auto_discover_backends do | ||
| case Workspaces.list_workspaces() do | ||
| [workspace | _] -> | ||
| {discover_workspace_backends(workspace.id), workspace.id} | ||
|
|
||
| [] -> | ||
| {[], nil} | ||
| end | ||
| rescue | ||
| e -> | ||
| Logger.debug("ModelRegistry: auto-discovery skipped: #{Exception.message(e)}") | ||
| {[], nil} | ||
| end | ||
|
|
||
| defp bootstrap_workspace_config(backend_specs, workspace_id, state) do | ||
| new_backends = | ||
| backend_specs | ||
| |> Enum.map(fn {backend, _} -> backend end) | ||
| |> Enum.concat(state.backends) | ||
| |> Enum.uniq() | ||
|
|
||
| new_configs = | ||
| Enum.reduce(backend_specs, state.backend_configs, fn {backend, secret_name}, acc -> | ||
| Map.update(acc, backend, %{secret_name: secret_name}, fn config -> | ||
| Map.put(config, :secret_name, secret_name) | ||
| end) | ||
| end) | ||
|
|
||
| new_last_probe = | ||
| Enum.reduce(new_backends, state.last_probe_at, fn backend, acc -> | ||
| Map.put_new(acc, backend, nil) | ||
| end) | ||
|
|
||
| %{ | ||
| state | ||
| | backends: new_backends, | ||
| backend_configs: new_configs, | ||
| workspace_id: workspace_id, | ||
| last_probe_at: new_last_probe | ||
| } | ||
| end | ||
|
|
||
| # ── Private — Probe result handling ───────────────────────── | ||
|
|
||
| defp handle_probe_result(backend, {:ok, []}, state) do | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.