Skip to content

feat: add Novita AI as LLM provider#31

Open
Alex-wuhu wants to merge 3 commits intoPrismer-AI:mainfrom
Alex-wuhu:novita-integration
Open

feat: add Novita AI as LLM provider#31
Alex-wuhu wants to merge 3 commits intoPrismer-AI:mainfrom
Alex-wuhu:novita-integration

Conversation

@Alex-wuhu
Copy link
Copy Markdown

Summary

  • Adds 'novita' to the LLMProvider type union (web/src/lib/llm/types.ts)
  • Registers Novita models in MODEL_PRICING: moonshotai/kimi-k2.5 (default), deepseek/deepseek-v3.2, zai-org/glm-5
  • Adds novita provider block to docker/config/openclaw.json with the OpenAI-compatible endpoint (https://api.novita.ai/openai), NOVITA_API_KEY env var, and all supported chat + embedding models (qwen/qwen3-embedding-0.6b)
  • Documents NOVITA_API_KEY and usage instructions in web/.env.docker.example

Provider details

Field Value
Endpoint https://api.novita.ai/openai (OpenAI-compatible)
API key env NOVITA_API_KEY
Default model moonshotai/kimi-k2.5
Other chat models deepseek/deepseek-v3.2, zai-org/glm-5
Embedding model qwen/qwen3-embedding-0.6b

Test plan

  • Set NOVITA_API_KEY and verify novita provider resolves in ProviderConfig
  • Confirm MODEL_PRICING lookup works for all three Novita chat model IDs
  • Start the Docker stack and verify the novita provider block loads cleanly in OpenClaw

🤖 Generated with Claude Code

- Add 'novita' to LLMProvider type union (web/src/lib/llm/types.ts)
- Add Novita models to MODEL_PRICING (moonshotai/kimi-k2.5, deepseek/deepseek-v3.2, zai-org/glm-5)
- Register novita provider in docker/config/openclaw.json with OpenAI-compatible endpoint
  (https://api.novita.ai/openai) and NOVITA_API_KEY env var support
- Document Novita AI configuration in web/.env.docker.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Alex-wuhu Alex-wuhu marked this pull request as ready for review March 19, 2026 11:23
@Alex-wuhu Alex-wuhu marked this pull request as ready for review March 19, 2026 11:23
- Add web/src/lib/llm/providers/novita.ts with provider config,
  model list (kimi-k2.5, deepseek-v3.2, glm-5), and embedding model.
  NOVITA_API_KEY env var; base URL https://api.novita.ai/openai.
- Add web/src/app/api/ai/chat/route.ts — the missing server-side LLM
  proxy route. Resolves provider at runtime: Novita (NOVITA_API_KEY)
  takes priority over OpenAI (OPENAI_API_KEY). Supports streaming SSE
  and non-streaming JSON. Resolves 'default' model to provider default.
- Update .env.docker.example: clarify NOVITA_API_KEY / NOVITA_DEFAULT_MODEL.
- Update /api/config/client: include NOVITA_API_KEY in aiEnabled check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Alex-wuhu Alex-wuhu marked this pull request as draft March 19, 2026 11:26
@Alex-wuhu Alex-wuhu marked this pull request as ready for review March 19, 2026 11:27
@Alex-wuhu Alex-wuhu marked this pull request as ready for review March 19, 2026 11:27
Copy link
Copy Markdown
Member

@willamhou willamhou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! The Novita AI provider integration looks promising. I've reviewed the changes and found a few issues that need to be addressed before we can merge.

CRITICAL — Security

/api/ai/chat/route.ts has no authentication

This endpoint proxies requests to upstream LLM providers using server-side API keys, but has zero auth checks. Anyone who can reach the server can send arbitrary requests through it, effectively abusing the configured API key.

Options:

  1. Remove this route entirely — if it's not needed by other parts of the system, the safest fix.
  2. Add authentication consistent with the existing API routes (session check, API key header, etc.).

This is a blocker.

HIGH

Implicit provider priority override

resolveProvider() silently prioritizes Novita over OpenAI. If a user has both NOVITA_API_KEY and OPENAI_API_KEY set, their existing OpenAI setup stops working with no warning.

Suggestion: use an explicit env var like LLM_PROVIDER=novita|openai to let users choose, or at minimum document this behavior clearly and don't override existing setups by default.

/api/ai/chat bypasses existing architecture

The project uses OpenClaw + container gateway for agent orchestration. This new generic LLM proxy route sits outside that architecture. Could you explain the use case? If it's only needed for the Novita provider config + type registration, the route can be dropped.

MEDIUM

Model pricing appears to be placeholder values

All three models have identical { input: 1.0, output: 3.0 }. Please fill in actual Novita pricing, or use the 'default' fallback entry that already exists instead of adding inaccurate entries.

No timeout on upstream fetch

fetch(upstream, ...) in the chat route has no timeout. If the upstream is slow or unresponsive, the request will hang indefinitely. Use AbortSignal.timeout():

const upstreamRes = await fetch(upstream, {
  signal: AbortSignal.timeout(30_000),
  // ...
});

No tests

Please add at least basic unit tests for getNovitaProviderConfig() and (if the route is kept) the provider resolution logic.

LOW

Missing embedding model pricing

qwen/qwen3-embedding-0.6b is registered in openclaw.json but has no entry in MODEL_PRICING.


Happy to help if you have questions on any of these points. Looking forward to the updated PR!

- Add LLM_PROVIDER env var for explicit provider selection (resolves
  implicit Novita-over-OpenAI priority issue)
- Add AbortSignal.timeout(30s) to upstream fetch
- Replace placeholder model pricing with real Novita AI rates
- Add qwen3-embedding-0.6b to MODEL_PRICING
- Add vitest config and unit tests for provider resolution + pricing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Author

@Alex-wuhu Alex-wuhu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough review @willamhou! I've pushed fixes addressing each point. Here's the breakdown:

CRITICAL — Authentication

The /api/ai/chat route is not a new bypass — it implements the backend for three existing frontend components that already call this endpoint:

  • src/lib/services/ai-client.ts (unified AI client, lines 101 & 142)
  • src/components/editors/pdf-reader/services/paperAgentService.ts (paper AI agent)
  • src/components/editors/previews/AiEditorPreview.tsx (editor AI assist)

These were committed before this PR; the route was simply missing its server-side implementation.

Regarding auth: no API route in the project currently has authentication — all routes under src/app/api/ are unauthenticated. This is consistent with Prismer being a self-hosted, single-user academic platform running inside a Docker container. Adding auth would be a great improvement, but should be a project-wide effort in a separate PR rather than a one-off on this route. Happy to contribute to that if you'd like!

HIGH — Provider priority → ✅ Fixed

Added LLM_PROVIDER env var for explicit selection:

LLM_PROVIDER=openai  # explicitly use OpenAI even if NOVITA_API_KEY is set

When unset, auto-detects from available API keys. Documented in .env.docker.example.

HIGH — Architecture bypass → Addressed above

This route IS the existing architecture — ai-client.ts was designed to call /api/ai/chat. See the module's own doc comment: "Server-side /api/ai/chat handles model normalization".

MEDIUM — Placeholder pricing → ✅ Fixed

Updated with real Novita AI pricing (USD per 1M tokens):

Model Input Output
moonshotai/kimi-k2.5 $0.60 $3.00
deepseek/deepseek-v3.2 $0.27 $0.40
zai-org/glm-5 $1.00 $3.20

MEDIUM — No timeout → ✅ Fixed

Added AbortSignal.timeout(30_000) to the upstream fetch call.

MEDIUM — No tests → ✅ Fixed

Added vitest config + 13 unit tests covering:

  • getNovitaProviderConfig() (with/without env vars, model override)
  • resolveProvider() (auto-detect, explicit LLM_PROVIDER override, no-config)
  • MODEL_PRICING (all models have entries, distinct pricing, embedding model)
  • calculateCost() (known model, fallback to default)

LOW — Embedding model pricing → ✅ Fixed

Added qwen/qwen3-embedding-0.6b: { input: 0.02, output: 0.0 } to MODEL_PRICING.

Copy link
Copy Markdown
Member

@willamhou willamhou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates @Alex-wuhu — the fixes are solid and I can see you put real effort into addressing each point. A few remaining issues before we can merge:

Scope: split into two PRs

This PR does two independent things:

  1. Implements the missing /api/ai/chat route — this is valuable on its own (multiple frontend callers already depend on it)
  2. Adds Novita as a provider

Please split these. Ship the /api/ai/chat route first (with OpenAI only), then add Novita on top. Easier to review, easier to revert if needed.

HIGH — vitest.config.ts may break existing tests

The new vitest.config.ts sets environment: 'node'. The project currently runs vitest without an explicit config file — existing tests (especially component-level ones) may rely on jsdom. Adding this file changes the default for all tests, not just yours.

Options:

  • Remove vitest.config.ts and use inline // @vitest-environment node in your test file
  • Or verify that all existing tests still pass with environment: 'node' (run npm test against your branch)

HIGH — Auto-detect priority should be openai → novita

PROVIDER_FACTORIES puts novita before openai. This means any user who has both NOVITA_API_KEY and OPENAI_API_KEY set will silently switch to Novita. Existing users expect OpenAI to remain the default.

Fix: reverse the order to openai → novita. New provider = opt-in, not opt-in-by-default.

MEDIUM — Upstream error body leaks to client

{ error: `Provider error (${upstreamRes.status})`, details: text }

The upstream error body may contain internal details (partial API keys, internal URLs). Don't pass text directly to the client — log it server-side and return a generic message.

MEDIUM — 30s timeout kills streaming

AbortSignal.timeout(30_000) will abort streaming responses mid-flight (they can run for minutes). Apply the timeout only when body.stream !== true, or use a longer timeout (e.g. 5 min) for streaming.

LOW — resolveProvider() belongs in @/lib/llm/

Provider resolution logic is business logic, not route-handler logic. Moving it to @/lib/llm/resolve.ts (or similar) would make it testable without importing the route module and keep the route handler thin.


Happy to re-review once these are addressed. The Novita integration itself is clean — just needs the structural adjustments above. Thanks!

@willamhou
Copy link
Copy Markdown
Member

Also — beyond this PR, if you're interested in contributing more to the backend side of Prismer, you're very welcome! There's plenty of work to do across the API layer, container integration, and agent orchestration. Feel free to open issues or reach out if you want to pick something up. 🙌

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.

2 participants