Skip to content

feat: support per-channel OpenAI-compatible LLM endpoints - #19

Open
jimmyzhuu wants to merge 1 commit into
Anil-matcha:mainfrom
jimmyzhuu:feat/openai-compatible-endpoints
Open

feat: support per-channel OpenAI-compatible LLM endpoints#19
jimmyzhuu wants to merge 1 commit into
Anil-matcha:mainfrom
jimmyzhuu:feat/openai-compatible-endpoints

Conversation

@jimmyzhuu

@jimmyzhuu jimmyzhuu commented Jun 26, 2026

Copy link
Copy Markdown

Summary

This adds per-channel support for OpenAI-compatible LLM endpoints through tools.toml.

Channels can now override not only the LiteLLM model name, but also the API base URL and the environment variable used for the API key. This makes it possible to route different Slack channels to different OpenAI-compatible gateways without changing application code or storing secrets in channel config files.

What changed

LLM configuration

tools.toml now supports these fields under [llm]:

[llm]
model = "openai/ernie-4.5-turbo-32k"
api_base = "https://qianfan.baidubce.com/v2"
api_key_env = "QIANFAN_API_KEY"

The runtime behavior is:

  • model overrides the global LLM_MODEL for that channel.
  • api_base is passed to litellm.acompletion.
  • api_key_env is resolved from the process environment and passed as api_key.
  • Explicit kwargs passed to acompletion(...) still take precedence over channel config.
  • If api_key_env is configured but the environment variable is missing, TagOpen fails fast with a clear configuration error instead of falling back to another provider key.

Docs and examples

  • Added README documentation for OpenAI-compatible endpoint configuration.
  • Added an example endpoint override in channels/example/tools.toml.
  • The example uses Baidu Qianfan's OpenAI-compatible endpoint shape with an ERNIE model and keeps the API key in an environment variable.

Test baseline fixes

While adding the new LLM tests, two existing test blockers surfaced:

  • tagopen/gateway/router.py referenced asyncio in a dataclass annotation before importing it, which caused test collection to fail.
  • MessageStore.get_recent_messages() sorted recent rows by created_at, which can be unstable for rapid inserts in SQLite. It now selects recent rows by autoincrement id, then returns them chronologically.

Why this is needed

Open Claude Tag already supports LiteLLM and per-channel model overrides, but many real deployments use OpenAI-compatible APIs behind custom gateways or cloud provider endpoints.

Without api_base and api_key_env, users have to rely on global provider environment variables or code changes to route a channel to a compatible endpoint. This change keeps the existing default path unchanged while making channel-level endpoint routing configurable.

User impact

A channel can now be configured to use a specific compatible endpoint:

[llm]
model = "openai/ernie-4.5-turbo-32k"
api_base = "https://qianfan.baidubce.com/v2"
api_key_env = "QIANFAN_API_KEY"

This keeps secrets out of tools.toml and allows different channels to use different endpoints or credentials.

Validation

Unit tests

SLACK_BOT_TOKEN=xoxb-test SLACK_APP_TOKEN=xapp-test python -m pytest tests/unit/test_llm.py -v

Result:

5 passed
SLACK_BOT_TOKEN=xoxb-test SLACK_APP_TOKEN=xapp-test python -m pytest tests/unit -v

Result:

13 passed

The new LLM tests cover:

  • global model fallback when no channel config exists
  • channel model override
  • api_base injection
  • api_key_env resolution
  • fail-fast behavior when api_key_env is configured but missing
  • explicit kwargs precedence over channel config
  • invalid tools.toml fallback behavior

Lint

ruff check tagopen/llm.py tagopen/gateway/router.py tagopen/memory/store.py tests/unit/test_llm.py

Result:

All checks passed

ruff check . still reports pre-existing lint issues in files outside this change. The files touched by this PR pass Ruff.

Real endpoint smoke test

A real smoke test passed against the Baidu Qianfan OpenAI-compatible endpoint using:

  • api_base: https://qianfan.baidubce.com/v2
  • model: openai/ernie-4.5-turbo-32k
  • credentials loaded through api_key_env

The smoke test used a temporary channel config and called the new tagopen.llm.acompletion(channel_id=...) path directly. The response returned non-empty assistant content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant