@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Ollama-specific context sizing, options, and context-error recovery policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable, Mapping, Set
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class OllamaRequestContextPolicy:
|
|
14
|
+
environ: Mapping[str, str]
|
|
15
|
+
positive_int: Callable[[Any], int | None]
|
|
16
|
+
estimate_tokens: Callable[[Any, dict[int, int] | None], int]
|
|
17
|
+
model_matches: Callable[[str, str | None], bool]
|
|
18
|
+
preset_names: Set[str]
|
|
19
|
+
default_request_timeout_ms: int
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def context_bucket(target: int, minimum: int, maximum: int) -> int:
|
|
23
|
+
target = max(minimum, min(maximum, target))
|
|
24
|
+
for bucket in (4096, 8192, 16384, 32768, 65536, 131072, 262144):
|
|
25
|
+
if bucket >= target:
|
|
26
|
+
return min(bucket, maximum)
|
|
27
|
+
return maximum
|
|
28
|
+
|
|
29
|
+
def provider_context_limit(self, config: dict[str, Any]) -> int | None:
|
|
30
|
+
current_model = str(config.get("current_model") or "")
|
|
31
|
+
cached_model = str(config.get("model_context_model") or "")
|
|
32
|
+
cached_limit = self.positive_int(config.get("model_context_max"))
|
|
33
|
+
if not cached_limit:
|
|
34
|
+
return None
|
|
35
|
+
if cached_model and (
|
|
36
|
+
not current_model or not self.model_matches(current_model, cached_model)
|
|
37
|
+
):
|
|
38
|
+
return None
|
|
39
|
+
return cached_limit
|
|
40
|
+
|
|
41
|
+
def preserve_configured_context_cap(self, config: dict[str, Any]) -> bool:
|
|
42
|
+
return str(config.get("llm_preset") or "").strip() in self.preset_names
|
|
43
|
+
|
|
44
|
+
def effective_context_limit(self, config: dict[str, Any]) -> int | None:
|
|
45
|
+
provider_limit = self.provider_context_limit(config)
|
|
46
|
+
configured_max = self.positive_int(config.get("num_ctx_max"))
|
|
47
|
+
if (
|
|
48
|
+
provider_limit
|
|
49
|
+
and configured_max
|
|
50
|
+
and self.preserve_configured_context_cap(config)
|
|
51
|
+
):
|
|
52
|
+
return min(provider_limit, configured_max)
|
|
53
|
+
return provider_limit or configured_max
|
|
54
|
+
|
|
55
|
+
def num_ctx_for_payload(
|
|
56
|
+
self,
|
|
57
|
+
config: dict[str, Any],
|
|
58
|
+
payload: Any,
|
|
59
|
+
_token_cache: dict[int, int] | None = None,
|
|
60
|
+
) -> int | None:
|
|
61
|
+
override = self.environ.get("CIEL_RUNTIME_OLLAMA_NUM_CTX")
|
|
62
|
+
if override:
|
|
63
|
+
return self.positive_int(override)
|
|
64
|
+
raw = config.get("num_ctx", "auto")
|
|
65
|
+
if isinstance(raw, str) and raw.strip().lower() in {"", "auto", "dynamic"}:
|
|
66
|
+
provider_limit = self.provider_context_limit(config)
|
|
67
|
+
if provider_limit:
|
|
68
|
+
return self.effective_context_limit(config) or provider_limit
|
|
69
|
+
# No model-card context is available (no /api/show max_model_len, no
|
|
70
|
+
# catalog/library match, no model-id hint). Do NOT invent a window:
|
|
71
|
+
# estimating from the payload and clamping to num_ctx_min/max used
|
|
72
|
+
# to send a guessed num_ctx the model card never advertised
|
|
73
|
+
# (operator 2026-07-29: if the model card does not provide
|
|
74
|
+
# num_ctx/num_predict, the parameter must be omitted so the server
|
|
75
|
+
# default applies — never substituted with our own guess).
|
|
76
|
+
return None
|
|
77
|
+
return self.positive_int(raw)
|
|
78
|
+
|
|
79
|
+
def num_predict_for_payload(
|
|
80
|
+
self,
|
|
81
|
+
config: dict[str, Any],
|
|
82
|
+
capped: int | None,
|
|
83
|
+
) -> int | None:
|
|
84
|
+
"""num_predict with model-card provenance gating.
|
|
85
|
+
|
|
86
|
+
An explicit user-configured value always passes through. But the
|
|
87
|
+
adapter-default ollama_options.num_predict (a heuristic, not a model
|
|
88
|
+
card value) is only sent when a model-card context exists for the
|
|
89
|
+
current model — otherwise the parameter is omitted and the server
|
|
90
|
+
default applies (operator 2026-07-29).
|
|
91
|
+
"""
|
|
92
|
+
value = self.positive_int(capped)
|
|
93
|
+
if not value:
|
|
94
|
+
return None
|
|
95
|
+
configured = self.positive_int(config.get("ollama_options", {}).get("num_predict") if isinstance(config.get("ollama_options"), dict) else None) or self.positive_int(config.get("max_output_tokens"))
|
|
96
|
+
if configured:
|
|
97
|
+
return value
|
|
98
|
+
if self.provider_context_limit(config):
|
|
99
|
+
return value
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def num_ctx_status(self, config: dict[str, Any]) -> str:
|
|
103
|
+
raw = config.get("num_ctx", "auto")
|
|
104
|
+
if isinstance(raw, str) and raw.strip().lower() in {"", "auto", "dynamic"}:
|
|
105
|
+
provider_limit = self.provider_context_limit(config)
|
|
106
|
+
if provider_limit:
|
|
107
|
+
effective_limit = self.effective_context_limit(config) or provider_limit
|
|
108
|
+
if effective_limit < provider_limit:
|
|
109
|
+
return f"auto ({effective_limit:,}; model max {provider_limit:,})"
|
|
110
|
+
return f"auto (provider {effective_limit:,})"
|
|
111
|
+
return "auto (server default — no model-card context)"
|
|
112
|
+
return str(self.positive_int(raw) or raw)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def extra_options(config: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
raw = config.get("ollama_options") or {}
|
|
117
|
+
if not isinstance(raw, dict):
|
|
118
|
+
return {}
|
|
119
|
+
return {str(key): value for key, value in raw.items() if value is not None}
|
|
120
|
+
|
|
121
|
+
def options_status(self, config: dict[str, Any]) -> str:
|
|
122
|
+
options = self.extra_options(config)
|
|
123
|
+
if not options:
|
|
124
|
+
return "{}"
|
|
125
|
+
return ", ".join(
|
|
126
|
+
f"{key}={json.dumps(value, ensure_ascii=False)}"
|
|
127
|
+
for key, value in sorted(options.items())
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def request_timeout_seconds(self, config: dict[str, Any]) -> float:
|
|
131
|
+
raw = config.get(
|
|
132
|
+
"request_timeout_ms",
|
|
133
|
+
config.get(
|
|
134
|
+
"request_timeout",
|
|
135
|
+
config.get("timeout_ms", self.default_request_timeout_ms),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
try:
|
|
139
|
+
value = float(raw)
|
|
140
|
+
except (TypeError, ValueError):
|
|
141
|
+
return 120.0
|
|
142
|
+
if value <= 0:
|
|
143
|
+
return 120.0
|
|
144
|
+
return max(1.0, value / 1000.0) if value > 10000 else value
|
|
145
|
+
|
|
146
|
+
def context_error_limit(self, raw: str | None) -> int | None:
|
|
147
|
+
text = str(raw or "")
|
|
148
|
+
normalized = text.lower()
|
|
149
|
+
if "context" not in normalized and "n_ctx" not in normalized:
|
|
150
|
+
return None
|
|
151
|
+
patterns = (
|
|
152
|
+
r"available context size\s*\(\s*(\d+)\s+tokens?\s*\)",
|
|
153
|
+
r'"n_ctx"\s*:\s*(\d+)',
|
|
154
|
+
r"\bn_ctx\s*[=:]\s*(\d+)",
|
|
155
|
+
)
|
|
156
|
+
for pattern in patterns:
|
|
157
|
+
match = re.search(pattern, text, re.IGNORECASE)
|
|
158
|
+
if match:
|
|
159
|
+
return self.positive_int(match.group(1))
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def context_retry_config(
|
|
163
|
+
self, config: dict[str, Any], context_limit: int
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
retry_config = dict(config)
|
|
166
|
+
context_limit = max(8192, int(context_limit))
|
|
167
|
+
retry_config["num_ctx"] = context_limit
|
|
168
|
+
retry_config["num_ctx_max"] = context_limit
|
|
169
|
+
minimum = self.positive_int(retry_config.get("num_ctx_min"))
|
|
170
|
+
if minimum and minimum > context_limit:
|
|
171
|
+
retry_config["num_ctx_min"] = context_limit
|
|
172
|
+
output_cap = max(256, min(2048, context_limit // 8))
|
|
173
|
+
configured_output = self.positive_int(retry_config.get("max_output_tokens"))
|
|
174
|
+
retry_config["max_output_tokens"] = (
|
|
175
|
+
min(configured_output, output_cap) if configured_output else output_cap
|
|
176
|
+
)
|
|
177
|
+
options = dict(self.extra_options(retry_config))
|
|
178
|
+
configured_num_predict = self.positive_int(options.get("num_predict"))
|
|
179
|
+
if configured_num_predict:
|
|
180
|
+
options["num_predict"] = min(configured_num_predict, output_cap)
|
|
181
|
+
retry_config["ollama_options"] = options
|
|
182
|
+
return retry_config
|
|
183
|
+
|
|
184
|
+
def context_limit_for_budget(self, config: dict[str, Any]) -> int:
|
|
185
|
+
raw = config.get("num_ctx", "auto")
|
|
186
|
+
if isinstance(raw, str) and raw.strip().lower() in {"", "auto", "dynamic"}:
|
|
187
|
+
return self.effective_context_limit(config) or 65536
|
|
188
|
+
return (
|
|
189
|
+
self.positive_int(raw)
|
|
190
|
+
or self.positive_int(config.get("num_ctx_max"))
|
|
191
|
+
or 65536
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = ["OllamaRequestContextPolicy"]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Ollama-specific runtime inspection and context output guard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class OllamaRuntimeServices:
|
|
11
|
+
request_base: Callable[[str, dict[str, Any]], str]
|
|
12
|
+
post_json: Callable[..., Any]
|
|
13
|
+
http_json: Callable[..., Any]
|
|
14
|
+
join_url: Callable[[str, str], str]
|
|
15
|
+
model_headers: Callable[[str, dict[str, Any]], dict[str, str]]
|
|
16
|
+
current_model: Callable[[str, dict[str, Any]], str]
|
|
17
|
+
positive_int: Callable[[Any], int | None]
|
|
18
|
+
model_context: Callable[[dict[str, Any]], int | None]
|
|
19
|
+
format_context: Callable[[int | None], str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OllamaRuntimeService:
|
|
23
|
+
def __init__(self, services: OllamaRuntimeServices) -> None:
|
|
24
|
+
self.services = services
|
|
25
|
+
|
|
26
|
+
def api_base(self, provider: str, config: dict[str, Any]) -> str:
|
|
27
|
+
base = self.services.request_base(provider, config)
|
|
28
|
+
return base[:-4].rstrip("/") if base.endswith("/api") else base.rstrip("/")
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def show_parameters(data: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
output: dict[str, Any] = {}
|
|
33
|
+
raw = data.get("parameters")
|
|
34
|
+
if isinstance(raw, dict):
|
|
35
|
+
output.update(raw)
|
|
36
|
+
elif isinstance(raw, str):
|
|
37
|
+
for line in raw.splitlines():
|
|
38
|
+
parts = line.strip().split(None, 1)
|
|
39
|
+
if len(parts) == 2 and not line.strip().startswith("#"):
|
|
40
|
+
output[parts[0].strip()] = parts[1].strip().strip('"')
|
|
41
|
+
modelfile = data.get("modelfile")
|
|
42
|
+
if isinstance(modelfile, str):
|
|
43
|
+
for line in modelfile.splitlines():
|
|
44
|
+
parts = line.strip().split(None, 2)
|
|
45
|
+
if (
|
|
46
|
+
len(parts) == 3
|
|
47
|
+
and not line.strip().startswith("#")
|
|
48
|
+
and parts[0].lower() == "parameter"
|
|
49
|
+
):
|
|
50
|
+
output.setdefault(parts[1].strip(), parts[2].strip().strip('"'))
|
|
51
|
+
return output
|
|
52
|
+
|
|
53
|
+
def fetch_model_specs(
|
|
54
|
+
self,
|
|
55
|
+
provider: str,
|
|
56
|
+
config: dict[str, Any],
|
|
57
|
+
model_id: str,
|
|
58
|
+
timeout: float = 3.0,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
if provider not in ("ollama", "ollama-cloud") or not model_id:
|
|
61
|
+
return {}
|
|
62
|
+
base = self.api_base(provider, config)
|
|
63
|
+
if not base:
|
|
64
|
+
return {}
|
|
65
|
+
data = self.services.post_json(
|
|
66
|
+
self.services.join_url(base, "/api/show"),
|
|
67
|
+
{"model": model_id},
|
|
68
|
+
headers=self.services.model_headers(provider, config),
|
|
69
|
+
timeout=timeout,
|
|
70
|
+
provider=provider,
|
|
71
|
+
pcfg=config,
|
|
72
|
+
)
|
|
73
|
+
if not isinstance(data, dict):
|
|
74
|
+
return {}
|
|
75
|
+
model_info = data.get("model_info") if isinstance(data.get("model_info"), dict) else {}
|
|
76
|
+
parameters = self.show_parameters(data)
|
|
77
|
+
max_context = (
|
|
78
|
+
self.services.model_context(data)
|
|
79
|
+
or self.services.model_context(model_info)
|
|
80
|
+
or self.services.positive_int(parameters.get("num_ctx"))
|
|
81
|
+
or self.services.positive_int(parameters.get("context_length"))
|
|
82
|
+
)
|
|
83
|
+
num_predict = self.services.positive_int(parameters.get("num_predict"))
|
|
84
|
+
output: dict[str, Any] = {}
|
|
85
|
+
if max_context:
|
|
86
|
+
output["max_model_len"] = max_context
|
|
87
|
+
if num_predict:
|
|
88
|
+
output["num_predict"] = num_predict
|
|
89
|
+
return output
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def model_id_matches(left: str, right: str) -> bool:
|
|
93
|
+
lhs = (left or "").strip().lower()
|
|
94
|
+
rhs = (right or "").strip().lower()
|
|
95
|
+
if lhs == rhs:
|
|
96
|
+
return True
|
|
97
|
+
return (lhs if ":" in lhs else f"{lhs}:latest") == (
|
|
98
|
+
rhs if ":" in rhs else f"{rhs}:latest"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def runtime_info(
|
|
102
|
+
self, config: dict[str, Any], timeout: float = 1.5
|
|
103
|
+
) -> dict[str, Any] | None:
|
|
104
|
+
base = self.api_base("ollama", config)
|
|
105
|
+
current = self.services.current_model("ollama", config)
|
|
106
|
+
if not base or not current:
|
|
107
|
+
return None
|
|
108
|
+
data = self.services.http_json(
|
|
109
|
+
self.services.join_url(base, "/api/ps"),
|
|
110
|
+
headers=self.services.model_headers("ollama", config),
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
)
|
|
113
|
+
items = data.get("models") if isinstance(data, dict) else None
|
|
114
|
+
if not isinstance(items, list):
|
|
115
|
+
return None
|
|
116
|
+
selected = next(
|
|
117
|
+
(
|
|
118
|
+
item
|
|
119
|
+
for item in items
|
|
120
|
+
if isinstance(item, dict)
|
|
121
|
+
and any(
|
|
122
|
+
self.model_id_matches(str(item.get(key) or ""), current)
|
|
123
|
+
for key in ("name", "model", "id")
|
|
124
|
+
)
|
|
125
|
+
),
|
|
126
|
+
None,
|
|
127
|
+
)
|
|
128
|
+
if not isinstance(selected, dict):
|
|
129
|
+
return None
|
|
130
|
+
details = selected.get("details") if isinstance(selected.get("details"), dict) else {}
|
|
131
|
+
return {
|
|
132
|
+
"requested_model": current,
|
|
133
|
+
"runtime_model": str(selected.get("name") or selected.get("model") or ""),
|
|
134
|
+
"loaded_context_len": self.services.positive_int(selected.get("context_length"))
|
|
135
|
+
or self.services.model_context(selected),
|
|
136
|
+
"size_vram": self.services.positive_int(selected.get("size_vram")),
|
|
137
|
+
"parameter_size": details.get("parameter_size"),
|
|
138
|
+
"quantization_level": details.get("quantization_level"),
|
|
139
|
+
"family": details.get("family"),
|
|
140
|
+
"families": details.get("families"),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def apply_output_guard(
|
|
144
|
+
self,
|
|
145
|
+
provider: str,
|
|
146
|
+
config: dict[str, Any],
|
|
147
|
+
runtime_info: Callable[[dict[str, Any]], dict[str, Any] | None] | None = None,
|
|
148
|
+
) -> list[str]:
|
|
149
|
+
if provider != "ollama":
|
|
150
|
+
return []
|
|
151
|
+
try:
|
|
152
|
+
info = runtime_info(config) if runtime_info else self.runtime_info(config)
|
|
153
|
+
except Exception:
|
|
154
|
+
return []
|
|
155
|
+
loaded_context = self.services.positive_int((info or {}).get("loaded_context_len"))
|
|
156
|
+
cap = self.output_cap(loaded_context)
|
|
157
|
+
if not cap:
|
|
158
|
+
return []
|
|
159
|
+
options = config.setdefault("ollama_options", {})
|
|
160
|
+
configured = self.services.positive_int(
|
|
161
|
+
options.get("num_predict")
|
|
162
|
+
) or self.services.positive_int(config.get("max_output_tokens"))
|
|
163
|
+
if not configured or configured <= cap:
|
|
164
|
+
return []
|
|
165
|
+
options["num_predict"] = cap
|
|
166
|
+
config["max_output_tokens"] = cap
|
|
167
|
+
model = str((info or {}).get("runtime_model") or config.get("current_model") or "")
|
|
168
|
+
return [
|
|
169
|
+
f"Ollama runtime context {self.services.format_context(loaded_context)} "
|
|
170
|
+
f"for {model or 'current model'}; output capped to {cap:,} tokens."
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
def output_cap(self, context_length: int | None) -> int | None:
|
|
174
|
+
context = self.services.positive_int(context_length)
|
|
175
|
+
return max(2048, min(8192, context // 16)) if context else None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class OllamaRuntimeApi:
|
|
180
|
+
"""Stable public adapter for late-bound Ollama runtime services."""
|
|
181
|
+
|
|
182
|
+
service_factory: Callable[[], OllamaRuntimeService]
|
|
183
|
+
|
|
184
|
+
def api_base(self, config: dict[str, Any]) -> str:
|
|
185
|
+
return self.service_factory().api_base("ollama", config)
|
|
186
|
+
|
|
187
|
+
def provider_api_base(self, provider: str, config: dict[str, Any]) -> str:
|
|
188
|
+
return self.service_factory().api_base(provider, config)
|
|
189
|
+
|
|
190
|
+
def show_parameters(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
191
|
+
return self.service_factory().show_parameters(data)
|
|
192
|
+
|
|
193
|
+
def fetch_model_specs(
|
|
194
|
+
self,
|
|
195
|
+
provider: str,
|
|
196
|
+
config: dict[str, Any],
|
|
197
|
+
model_id: str,
|
|
198
|
+
timeout: float = 3.0,
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
return self.service_factory().fetch_model_specs(
|
|
201
|
+
provider, config, model_id, timeout
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def model_id_matches(self, left: str, right: str) -> bool:
|
|
205
|
+
return self.service_factory().model_id_matches(left, right)
|
|
206
|
+
|
|
207
|
+
def runtime_info(
|
|
208
|
+
self, config: dict[str, Any], timeout: float = 1.5
|
|
209
|
+
) -> dict[str, Any] | None:
|
|
210
|
+
return self.service_factory().runtime_info(config, timeout)
|
|
211
|
+
|
|
212
|
+
def output_cap(self, context_length: int | None) -> int | None:
|
|
213
|
+
return self.service_factory().output_cap(context_length)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""OpenCode Zen provider adapter."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
from ..architecture import (
|
|
7
|
+
MessageProtocol,
|
|
8
|
+
ProviderCapabilities,
|
|
9
|
+
ProviderConfigurationPolicy,
|
|
10
|
+
ProviderConfig,
|
|
11
|
+
ProviderContextPolicy,
|
|
12
|
+
ProviderModelCatalogPolicy,
|
|
13
|
+
ProviderOptionPresentationPolicy,
|
|
14
|
+
ProviderRequestPolicy,
|
|
15
|
+
ProviderStatusPolicy,
|
|
16
|
+
)
|
|
17
|
+
from .base import (
|
|
18
|
+
HttpBearerProviderAdapter,
|
|
19
|
+
configuration_policy,
|
|
20
|
+
provider_configuration,
|
|
21
|
+
)
|
|
22
|
+
from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class OpenCodeProviderAdapter(HttpBearerProviderAdapter):
|
|
27
|
+
name: str = "opencode"
|
|
28
|
+
base_url: str = PROVIDER_DEFAULT_BASE_URLS["opencode"]
|
|
29
|
+
configuration_defaults_value: dict = field(
|
|
30
|
+
default_factory=lambda: provider_configuration(
|
|
31
|
+
"claude-sonnet-4-6",
|
|
32
|
+
custom_models=("claude-sonnet-4-6", "qwen3.6-plus-free"),
|
|
33
|
+
native_compat=True,
|
|
34
|
+
context_window=200000,
|
|
35
|
+
max_output_tokens=8192,
|
|
36
|
+
context_reserve_tokens=8192,
|
|
37
|
+
request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
|
|
38
|
+
stream_enabled=True,
|
|
39
|
+
stream_word_chunking=False,
|
|
40
|
+
ip_family="ipv6-preferred",
|
|
41
|
+
haiku_model="claude-haiku-4-5",
|
|
42
|
+
subagent_model="claude-sonnet-4-6",
|
|
43
|
+
model_endpoints={},
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
send_placeholder_key: bool = True
|
|
47
|
+
api_key_display_name_value: str = "OpenCode Zen"
|
|
48
|
+
api_key_launch_error_value: str = (
|
|
49
|
+
"Launch blocked: OpenCode Zen requires a OpenCode Zen API key."
|
|
50
|
+
)
|
|
51
|
+
capabilities_value: ProviderCapabilities = field(
|
|
52
|
+
default_factory=lambda: ProviderCapabilities(
|
|
53
|
+
upstream_protocol="anthropic_messages",
|
|
54
|
+
supports_thinking=True,
|
|
55
|
+
requires_api_key=True,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
request_policy_value: ProviderRequestPolicy = field(
|
|
59
|
+
default_factory=lambda: ProviderRequestPolicy(
|
|
60
|
+
chat_path="/messages",
|
|
61
|
+
models_path="/v1/models",
|
|
62
|
+
probe_strategy="opencode",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
model_catalog_policy_value: ProviderModelCatalogPolicy = field(
|
|
66
|
+
default_factory=lambda: ProviderModelCatalogPolicy(
|
|
67
|
+
kind="openai",
|
|
68
|
+
allow_configured_fallback=True,
|
|
69
|
+
allow_public_without_auth=True,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
|
|
74
|
+
del config
|
|
75
|
+
return ProviderContextPolicy(
|
|
76
|
+
capacity_strategy="configured_first",
|
|
77
|
+
settings_strategy="standard",
|
|
78
|
+
hosted_timeout=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def router_native_anthropic_enabled(
|
|
82
|
+
self, config: ProviderConfig, model: str | None = None
|
|
83
|
+
) -> bool:
|
|
84
|
+
return bool(config.options.get("native_compat", True)) and (
|
|
85
|
+
self.select_protocol("anthropic_messages", config, model)
|
|
86
|
+
== "anthropic_messages"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def option_presentation_policy(
|
|
90
|
+
self, config: ProviderConfig
|
|
91
|
+
) -> ProviderOptionPresentationPolicy:
|
|
92
|
+
del config
|
|
93
|
+
return ProviderOptionPresentationPolicy(
|
|
94
|
+
show_native=True,
|
|
95
|
+
show_tool_choice=True,
|
|
96
|
+
show_stream=True,
|
|
97
|
+
show_ip_family=True,
|
|
98
|
+
show_rate_limit_controls=True,
|
|
99
|
+
show_sampling_controls=True,
|
|
100
|
+
show_ip_family_control=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def select_protocol(
|
|
104
|
+
self,
|
|
105
|
+
operation: MessageProtocol,
|
|
106
|
+
config: ProviderConfig,
|
|
107
|
+
model: str | None = None,
|
|
108
|
+
) -> MessageProtocol:
|
|
109
|
+
del operation
|
|
110
|
+
raw_model = str(model or config.model or "").strip()
|
|
111
|
+
overrides = config.options.get("model_endpoints")
|
|
112
|
+
if isinstance(overrides, Mapping):
|
|
113
|
+
raw = overrides.get(raw_model)
|
|
114
|
+
key = str(raw or "").strip().lower().replace("_", "-")
|
|
115
|
+
mapped = {
|
|
116
|
+
"anthropic": "anthropic_messages",
|
|
117
|
+
"anthropic-messages": "anthropic_messages",
|
|
118
|
+
"messages": "anthropic_messages",
|
|
119
|
+
"openai": "openai_chat",
|
|
120
|
+
"openai-chat": "openai_chat",
|
|
121
|
+
"chat": "openai_chat",
|
|
122
|
+
"openai-responses": "openai_responses",
|
|
123
|
+
"responses": "openai_responses",
|
|
124
|
+
"google-generative": "google_generative",
|
|
125
|
+
"gemini": "google_generative",
|
|
126
|
+
}.get(key)
|
|
127
|
+
if mapped is not None:
|
|
128
|
+
return mapped
|
|
129
|
+
normalized = raw_model.split("[", 1)[0].lower()
|
|
130
|
+
for prefix in ("ciel-runtime-opencode-go-", "ciel-runtime-opencode-"):
|
|
131
|
+
if normalized.startswith(prefix):
|
|
132
|
+
normalized = normalized[len(prefix) :]
|
|
133
|
+
break
|
|
134
|
+
if self.name == "opencode-go":
|
|
135
|
+
if normalized.startswith(("glm-", "kimi-", "deepseek-", "mimo-", "hy3-")):
|
|
136
|
+
return "openai_chat"
|
|
137
|
+
return "anthropic_messages"
|
|
138
|
+
if normalized.startswith("gpt-"):
|
|
139
|
+
return "openai_responses"
|
|
140
|
+
if normalized.startswith("gemini-"):
|
|
141
|
+
return "google_generative"
|
|
142
|
+
if normalized.startswith(
|
|
143
|
+
(
|
|
144
|
+
"minimax-",
|
|
145
|
+
"glm-",
|
|
146
|
+
"kimi-",
|
|
147
|
+
"grok-",
|
|
148
|
+
"big-pickle",
|
|
149
|
+
"deepseek-",
|
|
150
|
+
"mimo-",
|
|
151
|
+
"nemotron-",
|
|
152
|
+
"north-",
|
|
153
|
+
)
|
|
154
|
+
):
|
|
155
|
+
return "openai_chat"
|
|
156
|
+
return "anthropic_messages"
|
|
157
|
+
|
|
158
|
+
def supported_protocols(
|
|
159
|
+
self, config: ProviderConfig, model: str | None = None
|
|
160
|
+
) -> frozenset[MessageProtocol]:
|
|
161
|
+
return frozenset({self.select_protocol("anthropic_messages", config, model)})
|
|
162
|
+
|
|
163
|
+
def openai_reasoning_passback_enabled(
|
|
164
|
+
self, config: ProviderConfig, model: str | None = None
|
|
165
|
+
) -> bool:
|
|
166
|
+
requested = self.normalize_model_id(str(model or ""))
|
|
167
|
+
prefix = f"ciel-runtime-{self.name}-"
|
|
168
|
+
if requested.startswith(prefix):
|
|
169
|
+
requested = requested[len(prefix) :]
|
|
170
|
+
elif requested.startswith("ciel-runtime-"):
|
|
171
|
+
requested = config.model
|
|
172
|
+
model_id = self.normalize_model_id(requested or config.model).lower()
|
|
173
|
+
return model_id.startswith("deepseek-") and self.select_protocol(
|
|
174
|
+
"openai_chat", config, model_id
|
|
175
|
+
) == "openai_chat"
|
|
176
|
+
|
|
177
|
+
def status_policy(self, config: ProviderConfig) -> ProviderStatusPolicy:
|
|
178
|
+
del config
|
|
179
|
+
return ProviderStatusPolicy(
|
|
180
|
+
kind="catalog",
|
|
181
|
+
label=self.api_key_display_name_value,
|
|
182
|
+
catalog_path="/v1/models",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def model_panel_badge(self, config: ProviderConfig, model: str) -> str:
|
|
186
|
+
protocol = self.select_protocol("anthropic_messages", config, model)
|
|
187
|
+
label = {
|
|
188
|
+
"anthropic_messages": "messages",
|
|
189
|
+
"openai_chat": "chat",
|
|
190
|
+
"openai_responses": "responses unsupported",
|
|
191
|
+
"google_generative": "gemini unsupported",
|
|
192
|
+
}.get(protocol, str(protocol))
|
|
193
|
+
overrides = config.options.get("model_endpoints")
|
|
194
|
+
if isinstance(overrides, Mapping) and model in overrides:
|
|
195
|
+
label += " override"
|
|
196
|
+
return label
|
|
197
|
+
|
|
198
|
+
def project_router_model_metadata(
|
|
199
|
+
self, config: ProviderConfig, model_id: str
|
|
200
|
+
) -> Mapping[str, object]:
|
|
201
|
+
protocol = self.select_protocol("anthropic_messages", config, model_id)
|
|
202
|
+
endpoint = {
|
|
203
|
+
"anthropic_messages": "anthropic-messages",
|
|
204
|
+
"openai_chat": "openai-chat",
|
|
205
|
+
"openai_responses": "openai-responses",
|
|
206
|
+
"google_generative": "google-generative",
|
|
207
|
+
}.get(protocol, str(protocol).replace("_", "-"))
|
|
208
|
+
return {
|
|
209
|
+
"opencode_endpoint": endpoint,
|
|
210
|
+
"router_supported": protocol in {"anthropic_messages", "openai_chat"},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def configuration_policy(
|
|
214
|
+
self, config: ProviderConfig
|
|
215
|
+
) -> ProviderConfigurationPolicy:
|
|
216
|
+
del config
|
|
217
|
+
return configuration_policy(supports_model_endpoint_overrides=True)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__ = ["OpenCodeProviderAdapter"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""OpenCode Go provider adapter."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .base import provider_configuration
|
|
6
|
+
from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
|
|
7
|
+
from .opencode import OpenCodeProviderAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class OpenCodeGoProviderAdapter(OpenCodeProviderAdapter):
|
|
12
|
+
name: str = "opencode-go"
|
|
13
|
+
base_url: str = PROVIDER_DEFAULT_BASE_URLS["opencode-go"]
|
|
14
|
+
configuration_defaults_value: dict = field(
|
|
15
|
+
default_factory=lambda: provider_configuration(
|
|
16
|
+
"qwen3.6-plus",
|
|
17
|
+
custom_models=("qwen3.6-plus",),
|
|
18
|
+
native_compat=True,
|
|
19
|
+
context_window=1048576,
|
|
20
|
+
max_output_tokens=8192,
|
|
21
|
+
context_reserve_tokens=8192,
|
|
22
|
+
request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
|
|
23
|
+
stream_enabled=True,
|
|
24
|
+
stream_word_chunking=False,
|
|
25
|
+
ip_family="ipv6-preferred",
|
|
26
|
+
haiku_model="qwen3.5-plus",
|
|
27
|
+
subagent_model="qwen3.6-plus",
|
|
28
|
+
model_endpoints={},
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
api_key_display_name_value: str = "OpenCode Go"
|
|
32
|
+
api_key_launch_error_value: str = (
|
|
33
|
+
"Launch blocked: OpenCode Go requires a OpenCode Go API key."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["OpenCodeGoProviderAdapter"]
|