@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

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.
Files changed (126) hide show
  1. package/ciel_runtime.py +2553 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -0,0 +1,207 @@
1
+ """Provider configuration and credential administration use cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable, Protocol
8
+
9
+
10
+ class CredentialRepository(Protocol):
11
+ def store(self, key: str) -> None: ...
12
+ def clear(self) -> None: ...
13
+
14
+
15
+ class OAuthRuntime(Protocol):
16
+ def token(self) -> str: ...
17
+ def action(self, action: str) -> list[str]: ...
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ProviderAdministrationInfrastructure:
22
+ nvidia_credentials: Callable[[], CredentialRepository]
23
+ copilot_oauth: Callable[[], OAuthRuntime]
24
+ output: Callable[..., Any]
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class ProviderAdministrationSelection:
29
+ provider_choice: Callable[[], Any]
30
+ provider_endpoint: Callable[[], Any]
31
+ model_selection: Callable[[], Any]
32
+ advisor_model_selection: Callable[[], Any]
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class ProviderAdministrationCredentials:
37
+ management: Callable[[], Any]
38
+ cli: Callable[[], Any]
39
+ configured_keys: Callable[[str, dict[str, Any]], list[str]]
40
+ mask: Callable[[str], str]
41
+ fingerprint: Callable[[str], str]
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class ProviderAdministrationPresentation:
46
+ status: Callable[[], Any]
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ProviderAdministrationContext:
51
+ infrastructure: ProviderAdministrationInfrastructure
52
+ selection: ProviderAdministrationSelection
53
+ credentials: ProviderAdministrationCredentials
54
+ presentation: ProviderAdministrationPresentation
55
+
56
+ def store_nvidia_api_key(self, key: str) -> None:
57
+ self.infrastructure.nvidia_credentials().store(key)
58
+
59
+ def clear_nvidia_api_key(self) -> None:
60
+ self.infrastructure.nvidia_credentials().clear()
61
+
62
+ def github_copilot_oauth_token(self) -> str:
63
+ return self.infrastructure.copilot_oauth().token()
64
+
65
+ def run_copilot_oauth_action(self, action: str) -> list[str]:
66
+ return self.infrastructure.copilot_oauth().action(action)
67
+
68
+ def cmd_copilot_oauth(self, args: argparse.Namespace) -> None:
69
+ for line in self.run_copilot_oauth_action(args.action):
70
+ self.infrastructure.output(line, flush=True)
71
+
72
+ def set_provider_config(self, provider: str) -> list[str]:
73
+ return self.selection.provider_choice().select_standard(provider)
74
+
75
+ def set_provider_choice_config(self, choice: str) -> list[str]:
76
+ return self.selection.provider_choice().select(choice)
77
+
78
+ def set_base_url_config(self, provider: str, url: str) -> list[str]:
79
+ return self.selection.provider_endpoint().set_base_url(provider, url)
80
+
81
+ def set_model_config(self, value: str) -> list[str]:
82
+ return self.selection.model_selection().select(value)
83
+
84
+ def set_advisor_model_config(self, value: str) -> list[str]:
85
+ return self.selection.advisor_model_selection().select(value)
86
+
87
+ def store_api_key_config(self, provider: str, key: str) -> list[str]:
88
+ return self.credentials.management().store_one(provider, key)
89
+
90
+ def clear_api_key_config(self, provider: str) -> list[str]:
91
+ return self.credentials.management().clear(provider)
92
+
93
+ def store_api_keys_config(
94
+ self, provider: str, keys: list[str]
95
+ ) -> list[str]:
96
+ return self.credentials.management().store_many(provider, keys)
97
+
98
+ def stored_api_key_mask(self, provider: str, pcfg: dict[str, Any]) -> str:
99
+ keys = self.credentials.configured_keys(provider, pcfg)
100
+ if not keys:
101
+ return "not set"
102
+ primary = (
103
+ f"{self.credentials.mask(keys[0])}; "
104
+ f"fp {self.credentials.fingerprint(keys[0])}"
105
+ )
106
+ if len(keys) == 1:
107
+ return primary
108
+ return f"{len(keys)} keys (round-robin; primary {primary})"
109
+
110
+ def store_api_key_input_config(
111
+ self, provider: str, raw_value: str
112
+ ) -> list[str]:
113
+ return self.credentials.management().store_input(provider, raw_value)
114
+
115
+ def cmd_set_api_key(self, args: argparse.Namespace) -> None:
116
+ self.credentials.cli().set_one(args)
117
+
118
+ def cmd_set_api_keys(self, args: argparse.Namespace) -> None:
119
+ self.credentials.cli().set_many(args)
120
+
121
+ def cmd_api_key(self, args: argparse.Namespace) -> None:
122
+ self.credentials.cli().manage(args)
123
+
124
+ def status_lines(self) -> list[str]:
125
+ return self.presentation.status().lines()
126
+
127
+ def cmd_status(self, _: argparse.Namespace) -> None:
128
+ self.infrastructure.output("\n".join(self.status_lines()))
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class ProviderAdministrationCompatibilityApi:
133
+ context: Callable[[], ProviderAdministrationContext]
134
+
135
+ def store_nvidia_api_key(self, key: str) -> None:
136
+ self.context().store_nvidia_api_key(key)
137
+
138
+ def clear_nvidia_api_key(self) -> None:
139
+ self.context().clear_nvidia_api_key()
140
+
141
+ def github_copilot_oauth_token(self) -> str:
142
+ return self.context().github_copilot_oauth_token()
143
+
144
+ def run_copilot_oauth_action(self, action: str) -> list[str]:
145
+ return self.context().run_copilot_oauth_action(action)
146
+
147
+ def cmd_copilot_oauth(self, args: argparse.Namespace) -> None:
148
+ self.context().cmd_copilot_oauth(args)
149
+
150
+ def set_provider_config(self, provider: str) -> list[str]:
151
+ return self.context().set_provider_config(provider)
152
+
153
+ def set_provider_choice_config(self, choice: str) -> list[str]:
154
+ return self.context().set_provider_choice_config(choice)
155
+
156
+ def set_base_url_config(self, provider: str, url: str) -> list[str]:
157
+ return self.context().set_base_url_config(provider, url)
158
+
159
+ def set_model_config(self, value: str) -> list[str]:
160
+ return self.context().set_model_config(value)
161
+
162
+ def set_advisor_model_config(self, value: str) -> list[str]:
163
+ return self.context().set_advisor_model_config(value)
164
+
165
+ def store_api_key_config(self, provider: str, key: str) -> list[str]:
166
+ return self.context().store_api_key_config(provider, key)
167
+
168
+ def clear_api_key_config(self, provider: str) -> list[str]:
169
+ return self.context().clear_api_key_config(provider)
170
+
171
+ def store_api_keys_config(
172
+ self, provider: str, keys: list[str]
173
+ ) -> list[str]:
174
+ return self.context().store_api_keys_config(provider, keys)
175
+
176
+ def stored_api_key_mask(self, provider: str, pcfg: dict[str, Any]) -> str:
177
+ return self.context().stored_api_key_mask(provider, pcfg)
178
+
179
+ def store_api_key_input_config(
180
+ self, provider: str, raw_value: str
181
+ ) -> list[str]:
182
+ return self.context().store_api_key_input_config(provider, raw_value)
183
+
184
+ def cmd_set_api_key(self, args: argparse.Namespace) -> None:
185
+ self.context().cmd_set_api_key(args)
186
+
187
+ def cmd_set_api_keys(self, args: argparse.Namespace) -> None:
188
+ self.context().cmd_set_api_keys(args)
189
+
190
+ def cmd_api_key(self, args: argparse.Namespace) -> None:
191
+ self.context().cmd_api_key(args)
192
+
193
+ def status_lines(self) -> list[str]:
194
+ return self.context().status_lines()
195
+
196
+ def cmd_status(self, args: argparse.Namespace) -> None:
197
+ self.context().cmd_status(args)
198
+
199
+
200
+ __all__ = [
201
+ "ProviderAdministrationCompatibilityApi",
202
+ "ProviderAdministrationContext",
203
+ "ProviderAdministrationCredentials",
204
+ "ProviderAdministrationInfrastructure",
205
+ "ProviderAdministrationPresentation",
206
+ "ProviderAdministrationSelection",
207
+ ]
@@ -64,6 +64,7 @@ def apply_ollama_option(
64
64
  _set_ollama_option_explicit(pcfg, "num_predict", False)
65
65
  elif key in ("keep_alive", "keepalive"):
66
66
  pcfg.pop("keep_alive", None)
67
+ pcfg.pop("keep_alive_explicit", None)
67
68
  elif key == "think":
68
69
  pcfg["think"] = False
69
70
  pcfg.pop("think_explicit", None)
@@ -120,8 +121,10 @@ def apply_ollama_option(
120
121
  if key in ("keep_alive", "keepalive"):
121
122
  if value is None:
122
123
  pcfg.pop("keep_alive", None)
124
+ pcfg.pop("keep_alive_explicit", None)
123
125
  else:
124
126
  pcfg["keep_alive"] = str(value)
127
+ pcfg["keep_alive_explicit"] = True
125
128
  return
126
129
  if key in ("timeout", "timeout_ms", "request_timeout", "request_timeout_ms"):
127
130
  fixed = positive_int(value)
@@ -0,0 +1,137 @@
1
+ """Provider model registry and cache lifecycle bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+ from .model_cache_lifecycle import (
10
+ ModelCacheLifecyclePorts,
11
+ ModelCacheLifecycleService,
12
+ )
13
+ from .model_registry_repository import (
14
+ ModelRegistryPaths,
15
+ ModelRegistryPolicy,
16
+ ModelRegistryRepository,
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ProviderModelRegistryConfig:
22
+ config_dir: Path
23
+ registry_path: Path
24
+ list_cache_path: Path
25
+ gateway_cache_path: Path
26
+ ttl_seconds: float
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class ProviderModelRegistryPorts:
31
+ cache_key: Callable[..., str]
32
+ unique_ids: Callable[..., list[str]]
33
+ normalize_id: Callable[..., str]
34
+ positive_int: Callable[..., int]
35
+ recommendations: Callable[..., dict[str, Any]]
36
+ log: Callable[[str, str], None]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ProviderModelCachePorts:
41
+ invalidate_config: Callable[[], None]
42
+ upstream_model_ids: Callable[..., list[str]]
43
+ catalog_model_ids: Callable[..., list[str]]
44
+ sorted_model_ids: Callable[..., list[str]]
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class ProviderModelCatalogCompatibilityPorts:
49
+ read_list_cache: Callable[..., list[str] | None]
50
+ read_registry_models: Callable[..., list[str] | None]
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class ProviderModelCatalogContext:
55
+ config: ProviderModelRegistryConfig
56
+ registry: ProviderModelRegistryPorts
57
+ cache: ProviderModelCachePorts
58
+ compatibility: ProviderModelCatalogCompatibilityPorts
59
+
60
+ def registry_repository(self) -> ModelRegistryRepository:
61
+ return ModelRegistryRepository(
62
+ paths=ModelRegistryPaths(
63
+ self.config.config_dir,
64
+ self.config.registry_path,
65
+ self.config.list_cache_path,
66
+ ),
67
+ policy=ModelRegistryPolicy(
68
+ cache_key=self.registry.cache_key,
69
+ unique_ids=self.registry.unique_ids,
70
+ normalize_id=self.registry.normalize_id,
71
+ positive_int=self.registry.positive_int,
72
+ recommendations=self.registry.recommendations,
73
+ log=self.registry.log,
74
+ ),
75
+ ttl_seconds=self.config.ttl_seconds,
76
+ )
77
+
78
+ def lifecycle_service(self) -> ModelCacheLifecycleService:
79
+ return ModelCacheLifecycleService(
80
+ ModelCacheLifecyclePorts(
81
+ invalidate_config=self.cache.invalidate_config,
82
+ artifact_paths=lambda: (
83
+ self.config.gateway_cache_path,
84
+ self.config.list_cache_path,
85
+ self.config.registry_path,
86
+ ),
87
+ read_list_cache=self.compatibility.read_list_cache,
88
+ read_registry_models=self.compatibility.read_registry_models,
89
+ upstream_model_ids=self.cache.upstream_model_ids,
90
+ catalog_model_ids=self.cache.catalog_model_ids,
91
+ normalize_model_id=self.registry.normalize_id,
92
+ unique_model_ids=self.registry.unique_ids,
93
+ sorted_model_ids=self.cache.sorted_model_ids,
94
+ log=self.registry.log,
95
+ )
96
+ )
97
+
98
+ def clear(self) -> None:
99
+ self.lifecycle_service().clear()
100
+
101
+ def cached_or_configured_ids(
102
+ self, provider: str, pcfg: dict[str, Any]
103
+ ) -> list[str]:
104
+ return self.lifecycle_service().cached_or_configured_ids(provider, pcfg)
105
+
106
+ def ensure_for_launch(self, provider: str, pcfg: dict[str, Any]) -> None:
107
+ self.lifecycle_service().ensure_for_launch(provider, pcfg)
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class ProviderModelCatalogCompatibilityApi:
112
+ context: Callable[[], ProviderModelCatalogContext]
113
+
114
+ def registry_repository(self) -> ModelRegistryRepository:
115
+ return self.context().registry_repository()
116
+
117
+ def lifecycle_service(self) -> ModelCacheLifecycleService:
118
+ return self.context().lifecycle_service()
119
+
120
+ def clear(self) -> None:
121
+ self.context().clear()
122
+
123
+ def cached_or_configured_ids(self, *args: Any, **kwargs: Any) -> list[str]:
124
+ return self.context().cached_or_configured_ids(*args, **kwargs)
125
+
126
+ def ensure_for_launch(self, *args: Any, **kwargs: Any) -> None:
127
+ self.context().ensure_for_launch(*args, **kwargs)
128
+
129
+
130
+ __all__ = [
131
+ "ProviderModelCachePorts",
132
+ "ProviderModelCatalogCompatibilityPorts",
133
+ "ProviderModelCatalogCompatibilityApi",
134
+ "ProviderModelCatalogContext",
135
+ "ProviderModelRegistryConfig",
136
+ "ProviderModelRegistryPorts",
137
+ ]
@@ -0,0 +1,107 @@
1
+ """Provider model context-capacity and output-budget bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from .architecture import ProviderContextPolicy
9
+ from .provider_context import ProviderContextServices
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ProviderModelContextQueries:
14
+ context_policy: Callable[[str, dict[str, Any]], ProviderContextPolicy]
15
+ context_limit: Callable[[str, dict[str, Any]], int | None]
16
+ positive_int: Callable[[Any], int | None]
17
+ format_context: Callable[[int | None], str]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ProviderModelContextAlgorithms:
22
+ resolve_capacity: Callable[..., int | None]
23
+ apply_capacity_cap: Callable[..., list[str]]
24
+ resolve_small_output_cap: Callable[..., int | None]
25
+ apply_output_token_cap: Callable[..., int | None]
26
+ apply_output_context_cap: Callable[..., list[str]]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class ProviderModelContext:
31
+ services: ProviderContextServices
32
+ queries: ProviderModelContextQueries
33
+ algorithms: ProviderModelContextAlgorithms
34
+
35
+ def capacity(self, provider: str, config: dict[str, Any]) -> int | None:
36
+ return self.algorithms.resolve_capacity(
37
+ provider,
38
+ config,
39
+ self.queries.context_policy(provider, config),
40
+ self.services,
41
+ )
42
+
43
+ def cap_context(self, provider: str, config: dict[str, Any]) -> list[str]:
44
+ return self.algorithms.apply_capacity_cap(
45
+ config,
46
+ self.capacity(provider, config),
47
+ self.queries.context_policy(provider, config),
48
+ positive_int=self.queries.positive_int,
49
+ )
50
+
51
+ def small_output_cap(self, context_window: int | None) -> int | None:
52
+ return self.algorithms.resolve_small_output_cap(
53
+ context_window, positive_int=self.queries.positive_int
54
+ )
55
+
56
+ def cap_output_tokens(
57
+ self, provider: str, config: dict[str, Any], configured: int | None
58
+ ) -> int | None:
59
+ return self.algorithms.apply_output_token_cap(
60
+ configured,
61
+ self.queries.context_policy(provider, config),
62
+ self.queries.context_limit(provider, config),
63
+ positive_int=self.queries.positive_int,
64
+ )
65
+
66
+ def cap_output_settings(
67
+ self, provider: str, config: dict[str, Any]
68
+ ) -> list[str]:
69
+ return self.algorithms.apply_output_context_cap(
70
+ config,
71
+ self.queries.context_policy(provider, config),
72
+ self.queries.context_limit(provider, config),
73
+ positive_int=self.queries.positive_int,
74
+ format_context=self.queries.format_context,
75
+ )
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class ProviderModelContextCompatibilityApi:
80
+ context: Callable[[], ProviderModelContext]
81
+
82
+ def capacity(self, provider: str, config: dict[str, Any]) -> int | None:
83
+ return self.context().capacity(provider, config)
84
+
85
+ def cap_context(self, provider: str, config: dict[str, Any]) -> list[str]:
86
+ return self.context().cap_context(provider, config)
87
+
88
+ def small_output_cap(self, context_window: int | None) -> int | None:
89
+ return self.context().small_output_cap(context_window)
90
+
91
+ def cap_output_tokens(
92
+ self, provider: str, config: dict[str, Any], configured: int | None
93
+ ) -> int | None:
94
+ return self.context().cap_output_tokens(provider, config, configured)
95
+
96
+ def cap_output_settings(
97
+ self, provider: str, config: dict[str, Any]
98
+ ) -> list[str]:
99
+ return self.context().cap_output_settings(provider, config)
100
+
101
+
102
+ __all__ = [
103
+ "ProviderModelContext",
104
+ "ProviderModelContextAlgorithms",
105
+ "ProviderModelContextCompatibilityApi",
106
+ "ProviderModelContextQueries",
107
+ ]
@@ -0,0 +1,197 @@
1
+ """Provider model cache identity, capability, and catalog header policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Mapping
9
+
10
+ from . import anthropic_model_policy
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class ModelCapabilityPorts:
15
+ normalize_capabilities: Callable[[Any], list[str]]
16
+ current_model: Callable[[str, dict[str, Any]], str]
17
+ strip_context_suffix: Callable[[str], str]
18
+ is_kimi_k3: Callable[[str], bool]
19
+ parse_bool: Callable[..., bool]
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class ModelRegistryRecommendationPorts:
24
+ unique_model_ids: Callable[[list[str]], list[str]]
25
+ preset_timeout_ms: Callable[[str], int]
26
+ timeout_idle_ms: Callable[[str], int]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class ModelCatalogHeaderPorts:
31
+ api_key_count: Callable[[str, dict[str, Any]], int]
32
+ read_env_file: Callable[[Path], dict[str, str]]
33
+ environment: Mapping[str, str]
34
+ user_agent_headers: Callable[..., dict[str, str]]
35
+ primary_api_key: Callable[[str, dict[str, Any]], str | None]
36
+ meaningful_key: Callable[[str | None], bool]
37
+ configured_adapter: Callable[..., Any]
38
+ contract_config: Callable[..., Any]
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class ProviderModelMetadataContext:
43
+ capabilities: ModelCapabilityPorts
44
+ recommendations: ModelRegistryRecommendationPorts
45
+ headers: ModelCatalogHeaderPorts
46
+ nvidia_env: Path
47
+
48
+ def cache_key(self, provider: str, pcfg: dict[str, Any]) -> str:
49
+ api_state = (
50
+ "key" if self.headers.api_key_count(provider, pcfg) else "nokey"
51
+ )
52
+ return json.dumps(
53
+ {
54
+ "provider": provider,
55
+ "base_url": pcfg.get("base_url", ""),
56
+ "model_api_base_url": pcfg.get("model_api_base_url", ""),
57
+ "account_id": pcfg.get("account_id", ""),
58
+ "api": api_state,
59
+ "custom": pcfg.get("custom_models", []),
60
+ "schema": 7,
61
+ },
62
+ sort_keys=True,
63
+ )
64
+
65
+ def infer_claude_capabilities(self, model_id: str) -> list[str]:
66
+ return anthropic_model_policy.infer_capabilities(
67
+ model_id, self.capabilities.strip_context_suffix
68
+ )
69
+
70
+ def claude_capabilities(
71
+ self,
72
+ provider: str,
73
+ pcfg: dict[str, Any],
74
+ model_id: str | None = None,
75
+ ) -> list[str]:
76
+ caps = self.capabilities.normalize_capabilities(
77
+ pcfg.get("claude_code_supported_capabilities")
78
+ )
79
+ model = model_id or self.capabilities.current_model(provider, pcfg)
80
+ if not caps:
81
+ caps = self.infer_claude_capabilities(model)
82
+ if (
83
+ provider == "kimi"
84
+ and self.capabilities.is_kimi_k3(model)
85
+ and "max_effort" not in caps
86
+ ):
87
+ caps.append("max_effort")
88
+ return caps
89
+
90
+ def claude_capability_string(
91
+ self,
92
+ provider: str,
93
+ pcfg: dict[str, Any],
94
+ model_id: str | None = None,
95
+ ) -> str:
96
+ return ",".join(self.claude_capabilities(provider, pcfg, model_id))
97
+
98
+ def workflows_enabled(self, provider: str, pcfg: dict[str, Any]) -> bool:
99
+ del provider
100
+ ultracode = (
101
+ pcfg.get("ultracode_enabled")
102
+ if "ultracode_enabled" in pcfg
103
+ else pcfg.get("ultracode")
104
+ )
105
+ if self.capabilities.parse_bool(ultracode, False):
106
+ return True
107
+ value = (
108
+ pcfg.get("workflows_enabled")
109
+ if "workflows_enabled" in pcfg
110
+ else pcfg.get("workflows")
111
+ )
112
+ return self.capabilities.parse_bool(value, False)
113
+
114
+ def ultracode_enabled(self, provider: str, pcfg: dict[str, Any]) -> bool:
115
+ del provider
116
+ value = (
117
+ pcfg.get("ultracode_enabled")
118
+ if "ultracode_enabled" in pcfg
119
+ else pcfg.get("ultracode")
120
+ )
121
+ return self.capabilities.parse_bool(value, False)
122
+
123
+ def registry_recommendations(
124
+ self, provider: str, models: list[str]
125
+ ) -> dict[str, Any]:
126
+ return anthropic_model_policy.AnthropicModelRecommendations(
127
+ self.recommendations.unique_model_ids,
128
+ self.recommendations.preset_timeout_ms,
129
+ self.recommendations.timeout_idle_ms,
130
+ ).build(provider, models)
131
+
132
+ def nvidia_list_headers(self) -> dict[str, str]:
133
+ headers = {"content-type": "application/json"}
134
+ key = self.headers.read_env_file(self.nvidia_env).get(
135
+ "NVIDIA_API_KEY"
136
+ ) or self.headers.environment.get("NVIDIA_API_KEY")
137
+ if key:
138
+ headers["authorization"] = f"Bearer {key}"
139
+ headers["x-api-key"] = key
140
+ return headers
141
+
142
+ def provider_list_headers(
143
+ self, provider: str, pcfg: dict[str, Any]
144
+ ) -> dict[str, str]:
145
+ headers = self.headers.user_agent_headers(
146
+ {"content-type": "application/json"}
147
+ )
148
+ key = self.headers.primary_api_key(provider, pcfg)
149
+ meaningful = str(key) if self.headers.meaningful_key(key) else None
150
+ adapter = self.headers.configured_adapter(provider, pcfg)
151
+ headers.update(
152
+ adapter.build_model_headers(
153
+ self.headers.contract_config(provider, pcfg), meaningful
154
+ )
155
+ )
156
+ return headers
157
+
158
+
159
+ @dataclass(frozen=True, slots=True)
160
+ class ProviderModelMetadataCompatibilityApi:
161
+ context: Callable[[], ProviderModelMetadataContext]
162
+
163
+ def cache_key(self, *args: Any, **kwargs: Any) -> str:
164
+ return self.context().cache_key(*args, **kwargs)
165
+
166
+ def infer_claude_capabilities(self, *args: Any, **kwargs: Any) -> list[str]:
167
+ return self.context().infer_claude_capabilities(*args, **kwargs)
168
+
169
+ def claude_capabilities(self, *args: Any, **kwargs: Any) -> list[str]:
170
+ return self.context().claude_capabilities(*args, **kwargs)
171
+
172
+ def claude_capability_string(self, *args: Any, **kwargs: Any) -> str:
173
+ return self.context().claude_capability_string(*args, **kwargs)
174
+
175
+ def workflows_enabled(self, *args: Any, **kwargs: Any) -> bool:
176
+ return self.context().workflows_enabled(*args, **kwargs)
177
+
178
+ def ultracode_enabled(self, *args: Any, **kwargs: Any) -> bool:
179
+ return self.context().ultracode_enabled(*args, **kwargs)
180
+
181
+ def registry_recommendations(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
182
+ return self.context().registry_recommendations(*args, **kwargs)
183
+
184
+ def nvidia_list_headers(self) -> dict[str, str]:
185
+ return self.context().nvidia_list_headers()
186
+
187
+ def provider_list_headers(self, *args: Any, **kwargs: Any) -> dict[str, str]:
188
+ return self.context().provider_list_headers(*args, **kwargs)
189
+
190
+
191
+ __all__ = [
192
+ "ModelCapabilityPorts",
193
+ "ModelCatalogHeaderPorts",
194
+ "ModelRegistryRecommendationPorts",
195
+ "ProviderModelMetadataCompatibilityApi",
196
+ "ProviderModelMetadataContext",
197
+ ]
@@ -220,13 +220,20 @@ class ProviderModelSelection:
220
220
  force_refresh: bool = False,
221
221
  ) -> tuple[bool, list[str]]:
222
222
  adapter = self.selection.adapter(provider, config)
223
- if not adapter.requires_catalog_model_selection(
224
- self.selection.contract(provider, config)
223
+ contract = self.selection.contract(provider, config)
224
+ catalog_policy = adapter.model_catalog_policy(contract)
225
+ if (
226
+ not adapter.requires_catalog_model_selection(contract)
227
+ and not catalog_policy.authoritative_upstream_catalog
225
228
  ):
226
229
  return True, []
227
230
  current = self.identity.normalize(provider, str(config.get("current_model") or ""))
228
231
  placeholders = self.selection.placeholders(provider)
229
- if current and current not in placeholders:
232
+ if (
233
+ current
234
+ and current not in placeholders
235
+ and not catalog_policy.authoritative_upstream_catalog
236
+ ):
230
237
  return True, []
231
238
  try:
232
239
  ids = self.selection.unique_ids(