Skip to content

feat(nebul): add Nebul provider with 16 models and hourly sync module - #6961

Closed
wynandhuizinga wants to merge 5 commits into
anomalyco:devfrom
wynandhuizinga:add-nebul-provider-v2
Closed

wynandhuizinga wants to merge 5 commits into
anomalyco:devfrom
wynandhuizinga:add-nebul-provider-v2

Conversation

@wynandhuizinga

Copy link
Copy Markdown

Add https://nebul.io as a provider with all 16 chat models it serves today, plus an hourly sync module.

Supersedes #6876 (rebased cleanly onto current dev; resolves its remaining review items). The Ministral 3 14B lab file landed upstream in the meantime, so the only new lab entry is Alibaba's Qwen3 30B A3B Instruct.

Closes #6879.

Sources

Data Verified against
Provider costs https://demos.nebul.io/pricing (USD per 1M tokens, matches live /model/info cost fields)
Reasoning effort values live GET https://api.inference.nebul.io/model/info "reasoning_efforts" is the per-model source of truth (DeepSeek V4.1 Flash: low|high|max; GLM-5.3-Flash: low|high|max; GLM-5.3: empty list; Mistral Medium 3.5: low|medium|high|max)
gpt-oss-120b high/medium/low via reasoning_effort Nebul docs (only model the docs demonstrate)
Wire fields Nebul docs: reasoning_effort param, no toggle, no budget; traces in message.reasoning

Reasoning controls policy

  • Effort-only where the host advertises values in /model/info or the docs demonstrate them (the four models above). All other reasoners use reasoning_options = [] with a sourced comment; no toggles, no budgets.
  • Sync preserves authored [] exactly; if nothing is advertised and nothing authored, new syncs fail closed with a missing-reasoning-controls error instead of inventing values.

Sync deletion safety

  • Deletes only happen when the host still serves enough entries (deleteDropped ≥10 live models / ≥50% retention), preserving existing catalog models otherwise.

Wynand Huizinga added 2 commits September 12, 2026 13:47
Adds the Nebul inference platform (https://nebul.io) as a provider:
- provider entry (npm @ai-sdk/openai-compatible, env NEBUL_API_KEY,
  base https://api.inference.nebul.io/v1)
- 16 served chat models with live costs/limits and per-model reasoning
  controls; effort values only where the live catalog
  (https://api.inference.nebul.io/model/info) "reasoning_efforts" or
  Nebul docs establish accepted values (DeepSeek V4.1 Flash, GLM-5.3
  Flash, Mistral Medium 3.5, gpt-oss-120b), empty otherwise; no toggles
  or budgets (undocumented)
- missing lab metadata for models/alibaba/qwen3-30b-a3b-instruct-2507
- an hourly sync module with delete safety, base_model alias autodiscovery,
  and fail-closed behavior for undocumented reasoning controls

Supersedes anomalyco#6876. Closes anomalyco#6879.
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/index.ts:225 - Check: Sync groups.direct must retain every registered provider already on base when adding a new one. Why: The proposed direct list inserts nebul but drops ollama-cloud, which is present on base ("meta", "ollama-cloud", "openai", …). That regresses local/grouped sync for Ollama Cloud. Action: Keep ollama-cloud in groups.direct and add nebul beside it (e.g. "meta", "nebul", "ollama-cloud", "openai", …).
  • [medium] [possible mistake] providers/nebul/models/Qwen/Qwen3.5-397B-A17B.toml:10 - Check: interleaved must match this host’s real reasoning side-channel wire path. Why: The PR body (and GLM-5.3 comments) say traces arrive in message.reasoning, and gemma/GLM correctly use bare interleaved = true for non-enum fields; most other Nebul reasoners set field = "reasoning_content" instead. Those shapes are not interchangeable for consumers. Action: Align every reasoner’s interleaved with Nebul’s actual response field: use interleaved = true for message.reasoning / inline traces, or field = "reasoning_content" only where that field is really returned—and make the set consistent across the 16 models.
  • [low] [possible mistake] providers/nebul/models/zai-org/GLM-5.3-Flash.toml:21 - Check: Host limit.context overrides should match the live catalog value. Why: Flash uses 1_048_572 while sibling GLM-5.3 uses 1_048_576 and the lab default is 1_000_000. 1_048_572 looks like a off-by-four typo of 1_048_576 unless /model/info really returns that exact number. Action: Confirm against Nebul max_input_tokens for zai-org/GLM-5.3-Flash and correct if it should be 1_048_576 (or the true API value).

…h context

- Each reasoner cites Nebul's Reasoning docs page for why its trace
  channel is reasoning_content, message.reasoning, or inline <thinking>
  (field is family-specific by design, not an inconsistency)
- GLM-5.3-Flash: note limit.context = 1,048,572 is the live
  GET /v1/model/info max_input_tokens for this exact ID (re-checked
  2026-09-13); sibling GLM-5.3 is 1,048,576
@wynandhuizinga

Copy link
Copy Markdown
Author

Action items

  • [high] [violation] packages/core/src/sync/index.ts:225 - Check: Sync groups.direct must retain every registered provider already on base when adding a new one. Why: The proposed direct list inserts nebul but drops ollama-cloud, which is present on base ("meta", "ollama-cloud", "openai", …). That regresses local/grouped sync for Ollama Cloud. Action: Keep ollama-cloud in groups.direct and add nebul beside it (e.g. "meta", "nebul", "ollama-cloud", "openai", …).
  • [medium] [possible mistake] providers/nebul/models/Qwen/Qwen3.5-397B-A17B.toml:10 - Check: interleaved must match this host’s real reasoning side-channel wire path. Why: The PR body (and GLM-5.3 comments) say traces arrive in message.reasoning, and gemma/GLM correctly use bare interleaved = true for non-enum fields; most other Nebul reasoners set field = "reasoning_content" instead. Those shapes are not interchangeable for consumers. Action: Align every reasoner’s interleaved with Nebul’s actual response field: use interleaved = true for message.reasoning / inline traces, or field = "reasoning_content" only where that field is really returned—and make the set consistent across the 16 models.
  • [low] [possible mistake] providers/nebul/models/zai-org/GLM-5.3-Flash.toml:21 - Check: Host limit.context overrides should match the live catalog value. Why: Flash uses 1_048_572 while sibling GLM-5.3 uses 1_048_576 and the lab default is 1_000_000. 1_048_572 looks like a off-by-four typo of 1_048_576 unless /model/info really returns that exact number. Action: Confirm against Nebul max_input_tokens for zai-org/GLM-5.3-Flash and correct if it should be 1_048_576 (or the true API value).
  1. groups.direct keeps both nebul and ollama-cloud as of the merge commit (line 228); the flagged hunk was from the pre-merge diff. Should clear on re-review.
  2. Interleaved fields are family-specific per your docs: reasoning_content (OpenAI-style/Nemotron/Qwen3-thinking) vs message.reasoning (GLM-5.x) vs inline (Gemma). Files now cite the Reasoning page; uniformity would misdescribe the API, and the schema's field enum makes bare true the only valid shape for GLM/Gemma.
  3. 1_048_572 is the live /v1/model/info max_input_tokens for GLM-5.3-Flash (sibling GLM-5.3 is 1_048_576) — verified 2026-09-13, noted in the file.

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/nebul.ts:63 - Check: Sync deletion safety for authoritative catalogs (sync.md; PR body claims retention guards). Why: The PR states deletes only run when the host still serves enough entries (deleteDropped ≥10 live / ≥50% retention), but nebul never sets deleteMissing: false and implements no empty/partial-catalog guard. With the runner default, a bad or empty /model/info response yields an empty desired set and deletes every local Nebul model (auto-merge’s post-hoc delete cap is not the same as in-sync retention). Action: Implement the claimed retention logic, or fail closed on empty/too-small chat catalogs (like llmgateway), or set deleteMissing: false and document why; align the PR body with the real behavior.
  • [medium] [possible mistake] providers/nebul/models/zai-org/GLM-5.3.toml:14 - Check: interleaved must match this host’s reasoning side channel (schema: true for inline content, or field = "reasoning_content" / "reasoning_details"). Why: Leading comments say GLM traces arrive in message.reasoning, but the files set interleaved = true (inline/<thinking> semantics). Lab and most peers use [interleaved] field = "reasoning_content". Clients will look in the wrong place. Same pattern on GLM-5.3-Flash.toml. Action: Verify the live Chat Completions message shape for both IDs; set field = "reasoning_content" if that is the side channel, or keep true only if traces are inline in content, and fix the comments to match.

deleteMissing defaults on for Nebul, so an empty or shape-drifted
/model/info response would sync as an empty desired set and delete
every local model file. Guard in parseModels (llmgateway-style):
throw when the catalog is empty or contains no usable chat models,
before the runner writes or deletes anything. Whole-catalog faults
abort; per-model transient nulls and unresolvable aliases still
retain existing entries. Document the behavior under Nebul Notes
in sync.md.
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/nebul.ts:72 - Check: Deletion safety for authoritative catalogs with deleteMissing on. Why: Nebul defaults to deleting local models absent from /model/info, but only rejects a fully empty or non-chat catalog. A partial/outage response (e.g. half the chat models) still builds a small desired set and removes every missing local TOML. The PR body claims a deleteDropped ≥10 / ≥50% retention guard that is not implemented. Action: Add a retention/min-live guard before the runner’s delete-missing pass (or set deleteMissing: false and document why), and align sync.md / the PR description with the real behavior.
  • [medium] [possible mistake] providers/nebul/models/moonshotai/Kimi-K3.toml:742 - Check: Host reasoning must match this API’s capability, not only lab inheritance. Why: The file inherits reasoning = true from moonshotai/kimi-k3, authors reasoning_options = [] plus an interleaved trace channel, and the leading comment states live /model/info marks this ID supports_reasoning = false. That is internally inconsistent: either the host does not reason (override reasoning = false, drop interleaved/reasoning_options) or it does (drop the false claim and keep a real control set / always-on [] with evidence). Action: Reconcile with the live supports_reasoning / efforts fields and either set reasoning = false or keep reasoner metadata without the contradictory claim; teach sync to honor supports_reasoning when present.
  • [low] [possible mistake] packages/core/src/sync/providers/nebul.ts:256 - Check: Preserve hand-authored provider-only lifecycle fields across sync. Why: translateModel refreshes cost / limit / reasoning_options / interleaved but never passes existing?.status, unlike OpenRouter/Kilo/Cortecs peers. A later hourly sync would strip any authored alpha/beta/deprecated marker. Action: Include status: existing?.status (and any other non-API provider-only fields you intend to keep) in the factored override payload.

@wynandhuizinga

Copy link
Copy Markdown
Author
  • [high] [violation] packages/core/src/sync/providers/nebul.ts:63 - Check: Sync deletion safety for authoritative catalogs (sync.md; PR body claims retention guards). Why: The PR states deletes only run when the host still serves enough entries (deleteDropped ≥10 live / ≥50% retention), but nebul never sets deleteMissing: false and implements no empty/partial-catalog guard. With the runner default, a bad or empty /model/info response yields an empty desired set and deletes every local Nebul model (auto-merge’s post-hoc delete cap is not the same as in-sync retention). Action: Implement the claimed retention logic, or fail closed on empty/too-small chat catalogs (like llmgateway), or set deleteMissing: false and document why; align the PR body with the real behavior.
  • [medium] [possible mistake] providers/nebul/models/zai-org/GLM-5.3.toml:14 - Check: interleaved must match this host’s reasoning side channel (schema: true for inline content, or field = "reasoning_content" / "reasoning_details"). Why: Leading comments say GLM traces arrive in message.reasoning, but the files set interleaved = true (inline/<thinking> semantics). Lab and most peers use [interleaved] field = "reasoning_content". Clients will look in the wrong place. Same pattern on GLM-5.3-Flash.toml. Action: Verify the live Chat Completions message shape for both IDs; set field = "reasoning_content" if that is the side channel, or keep true only if traces are inline in content, and fix the comments to match.

message.reasoning is not expressible via the schema's field enum (reasoning_content | reasoning_details only), so bare true is the correct shape for the GLM-5.x family on this host — same as bothub/GLM-5.3, runinfra/GLM-5.3-Flash, vercel/zai-glm-4.x, and bedrock/zai.glm-5 + minimax-m2.x, which all use interleaved = true for this trace channel. Lab entries cannot set interleaved at all (provider-only key). The inline- semantics apply only to gemma-4-31B-it here; each file's comment now cites the Reasoning docs page.

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.

Adding Nebul as provider for opencode

1 participant