Skip to content
Draft
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
119 changes: 111 additions & 8 deletions config/mini-chat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ gears:
config:
secrets:
- key: "azure-openai-key"
value: "${AZURE_OPENAI_API_KEY}"
value: "${AZURE_OPENAI_API_KEY:-}"
# Local H200 vLLM (gpt-oss). Add the value yourself.
- key: "vllm-h200-key"
value: "${VLLM_H200_API_KEY}"
# exa.ai web search. Add the value yourself.
- key: "exa-api-key"
value: "${EXA_API_KEY}"

file-parser:
config:
Expand Down Expand Up @@ -135,7 +141,7 @@ gears:
# `upstream_alias` is optional — defaults to `host` when omitted.
thread_summary_worker:
enabled: true
summary_model_id: "gpt-4.1-mini"
summary_model_id: "gpt-oss-120b"
compression_threshold_pct: 80
summary_system_prompt: "You are a conversation summarizer. Given a conversation (and optionally an existing summary), produce a detailed structured summary. Respond with an <analysis> block (your reasoning) followed by a <summary> block (the final summary). Only the <summary> content will be stored. Do not invent information not present in the conversation."
providers:
Expand All @@ -152,6 +158,38 @@ gears:
prefix: ""
secret_ref: "azure-openai-key"

# ── Local H200 vLLM (gpt-oss via the Responses API) ───────────────
# https://local-llm.adc.corp.acronis.com → POST /v1/responses
# (If this endpoint speaks Chat Completions instead, switch to
# kind: openai_chat_completions and api_path: /v1/chat/completions.)
vllm_h200:
kind: vllm_responses # vLLM omits the SSE `event:` field; its adapter compensates
storage_kind: openai # required; no RAG used for this model
host: "local-llm.adc.corp.acronis.com"
api_path: "/v1/responses"
supports_file_search_filters: false
auth_plugin_type: "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"
auth_config:
header: "Authorization"
prefix: "Bearer "
secret_ref: "vllm-h200-key"

# exa.ai web-search function tool. Models opt in via
# `enabled_function_tools: ["exa_search"]` in the model catalog below.
# Egress goes through OAGW (apikey auth injects the x-api-key header).
exa_search:
enabled: true
host: "api.exa.ai"
search_type: "auto"
num_results: 10
max_calls_per_message: 3
max_chars: 2000
auth_plugin_type: "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"
auth_config:
header: "x-api-key"
prefix: ""
secret_ref: "exa-api-key"

# ── Azure OpenAI example (uncomment and set your endpoint) ──────────
# azure_openai:
# kind: openai_responses
Expand Down Expand Up @@ -210,8 +248,8 @@ gears:
provider_model_id: "gpt-4.1"
display_name: "GPT-4.1"
description: "Most capable model"
provider_id: "azure_openai"
provider_display_name: "Azure OpenAI"
provider_id: "vllm_h200"
provider_display_name: "Local H200 (vLLM)"
icon: ""
tier: Premium
enabled: true
Expand Down Expand Up @@ -272,8 +310,8 @@ gears:
provider_model_id: "gpt-4.1-mini"
display_name: "GPT-4.1 Mini"
description: "Fast and efficient model"
provider_id: "azure_openai"
provider_display_name: "Azure OpenAI"
provider_id: "vllm_h200"
provider_display_name: "Local H200 (vLLM)"
icon: ""
tier: Standard
enabled: true
Expand Down Expand Up @@ -336,8 +374,8 @@ gears:
provider_model_id: "gpt-4.1-mini"
display_name: "GPT-4.1 Mini (Tiny Context - E2E)"
description: "E2E testing model with small context for thread summary trigger"
provider_id: "azure_openai"
provider_display_name: "Azure OpenAI"
provider_id: "vllm_h200"
provider_display_name: "Local H200 (vLLM)"
icon: ""
tier: Standard
enabled: true
Expand Down Expand Up @@ -393,6 +431,71 @@ gears:
is_default: false
sort_order: 99

# ── GPT-OSS 120B on local H200 (vLLM) with exa.ai web search ──
- id: "gpt-oss-120b"
provider_model_id: "h200/gpt-oss-120b"
display_name: "GPT-OSS 120B (H200)"
description: "Local gpt-oss-120b served by vLLM on the H200, with web search via exa.ai"
provider_id: "vllm_h200"
provider_display_name: "Local H200 (vLLM)"
icon: ""
tier: Standard
enabled: true
system_prompt: "You are a helpful, concise assistant. Answer clearly and directly."
thread_summary_prompt: ""
multimodal_capabilities: []
context_window: 131072
max_output_tokens: 32768
max_input_tokens: 131072
input_tokens_credit_multiplier_micro: 1000000
output_tokens_credit_multiplier_micro: 3000000
multiplier_display: "1x"
# Custom function tools enabled for this model (resolved against the
# gear's function-tool registry; `exa_search` is configured above).
enabled_function_tools: ["exa_search"]
estimation_budgets:
bytes_per_token_conservative: 4
fixed_overhead_tokens: 100
safety_margin_pct: 10
image_token_budget: 1000
tool_surcharge_tokens: 500
web_search_surcharge_tokens: 500
code_interpreter_surcharge_tokens: 1000
minimal_generation_floor: 50
max_num_results: 5
web_search_context_size: low
max_tool_calls: 10
general_config:
type: ""
available_from: "1970-01-01T00:00:00Z"
max_file_size_mb: 25
api_params:
temperature: 0.7
top_p: 1.0
frequency_penalty: 0.0
presence_penalty: 0.0
stop: []
features:
streaming: true
structured_output: true
tool_support:
web_search: false
file_search: false
image_generation: false
code_interpreter: false
mcp: false
supported_endpoints:
chat_completions: true
responses: true
embeddings: false
image_generation: false
audio_speech_generation: false
audio_transcription: false
audio_translation: false
preference:
is_default: false
sort_order: 2

oagw:
config:
proxy_timeout_secs: 60
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
status: accepted
date: 2026-06-25
---
# Pluggable Function Tools via a Generic Registry (exa.ai Web Search)

**ID**: `cpt-cf-mini-chat-adr-pluggable-function-tools`

## Context and Problem Statement

mini-chat already supports one custom LLM function tool — `search_knowledge` (RAG). Its
handling is hard-coded in the agentic provider loop: the loop matches the tool name literally
(`if name == "search_knowledge" { … } else { unexpected_tool_use → fail }`). Adding a second
tool (web search via exa.ai, requested for specific models) by copying that branch would
duplicate the per-tool limit, replay, and graceful-degradation logic for every future tool.

We need a way to (a) add new function tools without touching the loop, (b) enable them
per-model from configuration, and (c) call third-party tool providers (exa.ai) safely.

## Decision Drivers

* Extensibility — new tools should not require new branches in the agentic loop.
* Per-model control — only models that opt in (config) should advertise a given tool.
* Egress & credential management — third-party calls must be auditable and key-managed.
* Blast radius — avoid destabilising the working `search_knowledge` RAG path.

## Considered Options

* **A. Generic `FunctionTool` registry** — a domain trait + a `HashMap<name, Arc<dyn
FunctionTool>>`; the loop dispatches `function_call`s by name.
* **B. Hard-coded branch per tool** — add an `exa_search` branch beside `search_knowledge`.
* **C. Direct HTTP from the tool** vs. **egress through OAGW** (orthogonal sub-decision).

## Decision Outcome

Chosen: **A (generic registry)** with **egress through OAGW**.

* A domain port `FunctionTool { name, definition() -> LlmFunctionDef, system_prompt_guard(),
max_calls(), execute() }` decouples the loop from concrete tools. The loop keeps the existing
`search_knowledge` branch and adds **one** generic fall-through that looks the tool up in the
per-request registry, enforces `max_calls()` with the same soft-limit notice, executes, and
injects the `function_call_output`.
* Per-model enablement is a new `enabled_function_tools: Vec<String>` on `ModelCatalogEntry`
(CCM/policy-snapshot driven, `#[serde(default)]`). `StreamService` resolves the model's list
against the registry per request.
* `exa_search` is the first registered tool. It reaches exa.ai **through OAGW** (host + apikey
auth plugin injecting `x-api-key`), reusing the existing `RagHttpClient` JSON-POST primitive —
consistent with how LLM providers and RAG egress already work
(`gears/model-registry/docs/ADR/0003-…-oagw-provider-access`).
* Token accounting reuses the existing flat `tool_surcharge_tokens`: when a model has any
enabled function tool, preflight sets `tools_enabled = true`. No per-tool surcharge for now.
* The function descriptor authoring type is a new function-only `LlmFunctionDef`
(`definition()` returns it; `assemble_context` wraps it into `LlmTool::Function`).

### Consequences

* Good — new tools are added by implementing `FunctionTool` and registering; the loop is closed
to modification.
* Good — per-model gating lives in the policy catalog alongside other model capabilities.
* Good — exa credentials stay in OAGW; calls are audited via the existing egress path.
* Good — `search_knowledge` is untouched, so the RAG path is not destabilised.
* Bad / accepted trade-off — two mechanisms temporarily coexist: `search_knowledge` keeps its
bespoke branch (per-tool metrics, DB increments, `search_result` blocks) while new tools use
the registry. **Follow-up (phase 2):** migrate `search_knowledge` into the registry and
collapse `LlmTool::Function { … }` into `Function(LlmFunctionDef)` so there is one tool type
and one dispatch path.
* Bad / accepted — token cost of a tool's *output* is bounded by config (`max_chars`), not
preflighted (same as `search_knowledge` today).

### Confirmation

* Unit tests: `assemble_context` appends the tool descriptor + guard; `resolve_function_tools_from`
filters by the model's `enabled_function_tools`; `ExaSearchTool::format_results` against a
recorded exa response.
* Code review: the agentic loop has exactly one generic dispatch branch; unknown tool names
still fail via `unexpected_tool_use`.
* E2E: a model with `enabled_function_tools: ["exa_search"]` triggers a web-search tool call;
a model without the flag does not advertise the tool.

## Pros and Cons of the Options

* **A. Generic registry** — Good: extensible, testable, single dispatch path. Bad: one
indirection layer; interim coexistence with the `search_knowledge` branch.
* **B. Hard-coded branch** — Good: trivial for one tool. Bad: duplicates limit/replay/
degradation logic per tool; the loop grows with every tool.
* **C. Direct HTTP** — Good: fewer moving parts. Bad: bypasses centralised egress, credential
management, and audit; inconsistent with the rest of the stack. Rejected in favour of OAGW.
57 changes: 57 additions & 0 deletions gears/mini-chat/docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5027,6 +5027,53 @@ The surcharge model is **deterministic and independent of provider runtime behav
- P1 does **NOT** implement proportional infrastructure cost modelling. There is no per-invocation, per-query, or per-retrieval-pass cost tracking.
- Accurate backend cost accounting for tools, web search, and RAG operations is explicitly **out of scope** for P1. Future phases MAY introduce metered surcharges; any such change MUST be reflected via a new `policy_version`.

##### Custom Function Tools (Generic Registry + exa.ai Web Search)

Beyond the provider-native tools (`file_search`, `web_search`, `code_interpreter`) and the
built-in `search_knowledge` RAG function, mini-chat supports **pluggable custom function tools**
through a generic registry. This is how `exa_search` (web search via exa.ai) is added without
new branches in the agentic loop.

**Mechanism**

- Domain port `FunctionTool` (`domain/ports/function_tool.rs`):
`name()`, `definition() -> LlmFunctionDef`, `system_prompt_guard() -> Option<String>`,
`max_calls() -> u32`, `async execute(ctx, input) -> Result<String, FunctionToolError>`.
- The gear builds a registry `HashMap<name, Arc<dyn FunctionTool>>` at init and injects it into
`StreamService`. Per request, `resolve_function_tools` intersects the model's
`enabled_function_tools` with the registry, yielding the tool descriptors (appended to the LLM
request), their system-prompt guards (appended to the system instructions), and a dispatch map.
- The agentic loop in `provider_task.rs` keeps the bespoke `search_knowledge` branch and adds one
**generic** dispatch: on a `function_call` whose name is in the dispatch map, it enforces the
tool's `max_calls()` per-message limit (soft-limit notice on overflow, same graceful degradation
as `search_knowledge`), calls `execute`, and injects the result as `function_call_output`.
Unknown names still fail via `unexpected_tool_use`.

**Per-model enablement**

`ModelCatalogEntry.enabled_function_tools: Vec<String>` (policy-snapshot / CCM driven) lists the
tool names a model may call, e.g. `["exa_search"]`. Models without the entry never see the tool.

**exa_search tool**

- Descriptor: `{ name: "exa_search", description: "Search the public web …",
parameters: { query: string (required) } }`.
- Egress through OAGW: `POST /{alias}/search` with body
`{ "query", "type": <search_type>, "numResults": <n>, "contents": { "highlights": true } }`.
The OAGW apikey auth plugin injects the `x-api-key` header — the key lives in the credstore,
never in mini-chat. Reuses the `RagHttpClient` JSON-POST primitive.
- Response `results[]{ title, url, highlights[] }` is formatted into a compact text block
(bounded by `exa_search.max_chars`) and returned as the `function_call_output`.
- Reference: <https://docs.exa.ai/reference/search-api-guide-for-coding-agents>.

**Token cost.** Custom function tools reuse the existing flat `tool_surcharge_tokens`: when a
model has any enabled function tool, preflight sets `tools_enabled = true` (no per-tool
surcharge). The tool's output is bounded by `max_chars`, not separately preflighted — consistent
with `search_knowledge`.

> See ADR `cpt-cf-mini-chat-adr-pluggable-function-tools`. Phase-2 follow-up: migrate
> `search_knowledge` into the registry and collapse `LlmTool::Function` onto `LlmFunctionDef`.

#### 5.5.7 Reserved Credits Calculation

After estimation (canonical form — identical to section 5.4.1):
Expand Down Expand Up @@ -6544,6 +6591,15 @@ Legend:
| Parameter | Type | Default | Source | Notes |
|-----------|------|---------|--------|-------|
| `mini-chat.url_prefix` | `string` | `/mini-chat` | ConfigMap | ToolKit gear config, no CCM API equivalent |
| `mini-chat.exa_search.enabled` | `bool` | `false` | ConfigMap | Enables the `exa_search` web-search function tool |
| `mini-chat.exa_search.host` | `string` | `api.exa.ai` | ConfigMap | exa upstream host (OAGW registers it + a `POST /search` route) |
| `mini-chat.exa_search.upstream_alias` | `string?` | host | ConfigMap | OAGW alias; defaults to `host` |
| `mini-chat.exa_search.auth_plugin_type` / `auth_config` | object | — | ConfigMap | OAGW apikey auth (`header: x-api-key`, `secret_ref`); key lives in the credstore |
| `mini-chat.exa_search.search_type` | `string` | `auto` | ConfigMap | exa search type (`auto`/`fast`/`deep`/…) |
| `mini-chat.exa_search.num_results` | `integer` | `10` | ConfigMap | `numResults` per call |
| `mini-chat.exa_search.max_calls_per_message` | `integer` | `3` | ConfigMap | Soft per-message call limit (graceful degradation) |
| `mini-chat.exa_search.max_chars` | `integer` | `2000` | ConfigMap | Max characters kept per formatted result |
| `mini-chat.exa_search.guard` | `string` | built-in | ConfigMap | System-prompt guard appended when the tool is enabled |

## B.2 Policy / Models / Limits (via `mini-chat-model-policy-plugin`)

Expand Down Expand Up @@ -6575,6 +6631,7 @@ All fields below are per-model entries inside the catalog.
| `api_params.*` | object | **CCM API**: `GET /policies/{v}` | `snapshot.model_catalog[].general_config.api_params` |
| `features.*` | object | **CCM API**: `GET /policies/{v}` | `snapshot.model_catalog[].general_config.features` |
| `tool_support.*` | object | **CCM API**: `GET /policies/{v}` | `snapshot.model_catalog[].general_config.tool_support` |
| `enabled_function_tools` | `string[]` | **CCM API**: `GET /policies/{v}` | `snapshot.model_catalog[].enabled_function_tools` — custom function tools the model may call (e.g. `["exa_search"]`); resolved against the gear's function-tool registry, defaults to `[]` |
| `sort_order` | `integer` | **CCM API**: `GET /policies/{v}` | `snapshot.model_catalog[].preference.sort_order` |

### B.2.3 Kill switches / emergency flags
Expand Down
6 changes: 6 additions & 0 deletions gears/mini-chat/mini-chat-sdk/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ pub struct ModelCatalogEntry {
/// Maximum tool calls the provider may make per request.
#[serde(default = "default_max_tool_calls")]
pub max_tool_calls: u32,
/// Names of custom function tools enabled for this model (e.g.
/// `["exa_search"]`). Resolved against the gear's function-tool registry at
/// request time; unknown names are ignored. Empty = no custom tools.
#[serde(default)]
pub enabled_function_tools: Vec<String>,
/// Full general config captured at snapshot time.
pub general_config: ModelGeneralConfig,
/// Tenant preference settings captured at snapshot time.
Expand Down Expand Up @@ -387,6 +392,7 @@ mod tests {
}),
system_prompt: String::new(),
thread_summary_prompt: String::new(),
enabled_function_tools: Vec::new(),
}
}

Expand Down
Loading
Loading