devcopilot 0.2.0__py3-none-any.whl
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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- providers/zai/request.py +42 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""NVIDIA NIM provider implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import openai
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from config.nim import NimSettings
|
|
10
|
+
from providers.base import ProviderConfig
|
|
11
|
+
from providers.defaults import NVIDIA_NIM_DEFAULT_BASE
|
|
12
|
+
from providers.transports.openai_chat import OpenAIChatTransport
|
|
13
|
+
|
|
14
|
+
from .request import (
|
|
15
|
+
body_without_nim_tool_argument_aliases,
|
|
16
|
+
build_request_body,
|
|
17
|
+
clone_body_without_chat_template,
|
|
18
|
+
clone_body_without_reasoning_budget,
|
|
19
|
+
clone_body_without_reasoning_content,
|
|
20
|
+
nim_tool_argument_aliases_from_body,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NvidiaNimProvider(OpenAIChatTransport):
|
|
25
|
+
"""NVIDIA NIM provider using official OpenAI client."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: ProviderConfig, *, nim_settings: NimSettings):
|
|
28
|
+
super().__init__(
|
|
29
|
+
config,
|
|
30
|
+
provider_name="NIM",
|
|
31
|
+
base_url=config.base_url or NVIDIA_NIM_DEFAULT_BASE,
|
|
32
|
+
api_key=config.api_key,
|
|
33
|
+
)
|
|
34
|
+
self._nim_settings = nim_settings
|
|
35
|
+
|
|
36
|
+
def _build_request_body(
|
|
37
|
+
self, request: Any, thinking_enabled: bool | None = None
|
|
38
|
+
) -> dict:
|
|
39
|
+
"""Internal helper for tests and shared building."""
|
|
40
|
+
return build_request_body(
|
|
41
|
+
request,
|
|
42
|
+
self._nim_settings,
|
|
43
|
+
thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _prepare_create_body(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
"""Strip private request metadata before calling NVIDIA NIM."""
|
|
48
|
+
return body_without_nim_tool_argument_aliases(body)
|
|
49
|
+
|
|
50
|
+
def _tool_argument_aliases(self, body: dict[str, Any]) -> dict[str, dict[str, str]]:
|
|
51
|
+
"""Return NIM tool argument aliases captured while building this request."""
|
|
52
|
+
return nim_tool_argument_aliases_from_body(body)
|
|
53
|
+
|
|
54
|
+
def _get_retry_request_body(self, error: Exception, body: dict) -> dict | None:
|
|
55
|
+
"""Retry once with a downgraded body when NIM rejects a known field."""
|
|
56
|
+
status_code = getattr(error, "status_code", None)
|
|
57
|
+
if not isinstance(error, openai.BadRequestError) and status_code != 400:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
error_text = str(error)
|
|
61
|
+
error_body = getattr(error, "body", None)
|
|
62
|
+
if error_body is not None:
|
|
63
|
+
error_text = f"{error_text} {json.dumps(error_body, default=str)}"
|
|
64
|
+
error_text = error_text.lower()
|
|
65
|
+
|
|
66
|
+
if "reasoning_budget" in error_text:
|
|
67
|
+
retry_body = clone_body_without_reasoning_budget(body)
|
|
68
|
+
if retry_body is None:
|
|
69
|
+
return None
|
|
70
|
+
logger.warning(
|
|
71
|
+
"NIM_STREAM: retrying without reasoning_budget after 400 error"
|
|
72
|
+
)
|
|
73
|
+
return retry_body
|
|
74
|
+
|
|
75
|
+
if "chat_template" in error_text:
|
|
76
|
+
retry_body = clone_body_without_chat_template(body)
|
|
77
|
+
if retry_body is None:
|
|
78
|
+
return None
|
|
79
|
+
logger.warning("NIM_STREAM: retrying without chat_template after 400 error")
|
|
80
|
+
return retry_body
|
|
81
|
+
|
|
82
|
+
if "reasoning_content" in error_text:
|
|
83
|
+
retry_body = clone_body_without_reasoning_content(body)
|
|
84
|
+
if retry_body is None:
|
|
85
|
+
return None
|
|
86
|
+
logger.warning(
|
|
87
|
+
"NIM_STREAM: retrying without reasoning_content after 400 error"
|
|
88
|
+
)
|
|
89
|
+
return retry_body
|
|
90
|
+
|
|
91
|
+
return None
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Request builder for NVIDIA NIM provider."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from config.nim import NimSettings
|
|
10
|
+
from core.anthropic import (
|
|
11
|
+
ReasoningReplayMode,
|
|
12
|
+
build_base_request_body,
|
|
13
|
+
set_if_not_none,
|
|
14
|
+
)
|
|
15
|
+
from core.anthropic.conversion import OpenAIConversionError
|
|
16
|
+
from providers.exceptions import InvalidRequestError
|
|
17
|
+
|
|
18
|
+
_SCHEMA_VALUE_KEYS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"additionalProperties",
|
|
21
|
+
"additionalItems",
|
|
22
|
+
"unevaluatedProperties",
|
|
23
|
+
"unevaluatedItems",
|
|
24
|
+
"items",
|
|
25
|
+
"contains",
|
|
26
|
+
"propertyNames",
|
|
27
|
+
"if",
|
|
28
|
+
"then",
|
|
29
|
+
"else",
|
|
30
|
+
"not",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
_SCHEMA_LIST_KEYS = frozenset({"allOf", "anyOf", "oneOf", "prefixItems"})
|
|
34
|
+
_SCHEMA_MAP_KEYS = frozenset(
|
|
35
|
+
{"properties", "patternProperties", "$defs", "definitions", "dependentSchemas"}
|
|
36
|
+
)
|
|
37
|
+
NIM_TOOL_ARGUMENT_ALIASES_KEY = "_fcc_nim_tool_argument_aliases"
|
|
38
|
+
_NIM_TOOL_PARAMETER_ALIAS_PREFIX = "_fcc_arg_"
|
|
39
|
+
_NIM_UNSAFE_TOOL_PARAMETER_NAMES = frozenset({"type"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _clone_strip_extra_body(
|
|
43
|
+
body: dict[str, Any],
|
|
44
|
+
strip: Callable[[dict[str, Any]], bool],
|
|
45
|
+
) -> dict[str, Any] | None:
|
|
46
|
+
"""Deep-clone ``body`` and remove fields via ``strip`` on ``extra_body`` only.
|
|
47
|
+
|
|
48
|
+
Returns ``None`` when there is no ``extra_body`` dict or ``strip`` reports no change.
|
|
49
|
+
"""
|
|
50
|
+
cloned_body = deepcopy(body)
|
|
51
|
+
extra_body = cloned_body.get("extra_body")
|
|
52
|
+
if not isinstance(extra_body, dict):
|
|
53
|
+
return None
|
|
54
|
+
if not strip(extra_body):
|
|
55
|
+
return None
|
|
56
|
+
if not extra_body:
|
|
57
|
+
cloned_body.pop("extra_body", None)
|
|
58
|
+
return cloned_body
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _strip_reasoning_budget_fields(extra_body: dict[str, Any]) -> bool:
|
|
62
|
+
removed = extra_body.pop("reasoning_budget", None) is not None
|
|
63
|
+
chat_template_kwargs = extra_body.get("chat_template_kwargs")
|
|
64
|
+
if (
|
|
65
|
+
isinstance(chat_template_kwargs, dict)
|
|
66
|
+
and chat_template_kwargs.pop("reasoning_budget", None) is not None
|
|
67
|
+
):
|
|
68
|
+
removed = True
|
|
69
|
+
return removed
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _strip_chat_template_field(extra_body: dict[str, Any]) -> bool:
|
|
73
|
+
return extra_body.pop("chat_template", None) is not None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _strip_message_reasoning_content(body: dict[str, Any]) -> bool:
|
|
77
|
+
removed = False
|
|
78
|
+
messages = body.get("messages")
|
|
79
|
+
if not isinstance(messages, list):
|
|
80
|
+
return False
|
|
81
|
+
for message in messages:
|
|
82
|
+
if (
|
|
83
|
+
isinstance(message, dict)
|
|
84
|
+
and message.pop("reasoning_content", None) is not None
|
|
85
|
+
):
|
|
86
|
+
removed = True
|
|
87
|
+
return removed
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _sanitize_nim_schema_node(value: Any) -> tuple[bool, Any]:
|
|
91
|
+
"""Remove boolean JSON Schema subschemas that hosted NIM rejects."""
|
|
92
|
+
if isinstance(value, bool):
|
|
93
|
+
return False, None
|
|
94
|
+
if isinstance(value, dict):
|
|
95
|
+
sanitized: dict[str, Any] = {}
|
|
96
|
+
for key, item in value.items():
|
|
97
|
+
if key in _SCHEMA_VALUE_KEYS:
|
|
98
|
+
keep, sanitized_item = _sanitize_nim_schema_node(item)
|
|
99
|
+
if keep:
|
|
100
|
+
sanitized[key] = sanitized_item
|
|
101
|
+
elif key in _SCHEMA_LIST_KEYS and isinstance(item, list):
|
|
102
|
+
sanitized_items: list[Any] = []
|
|
103
|
+
for schema_item in item:
|
|
104
|
+
keep, sanitized_item = _sanitize_nim_schema_node(schema_item)
|
|
105
|
+
if keep:
|
|
106
|
+
sanitized_items.append(sanitized_item)
|
|
107
|
+
if sanitized_items:
|
|
108
|
+
sanitized[key] = sanitized_items
|
|
109
|
+
elif key in _SCHEMA_MAP_KEYS and isinstance(item, dict):
|
|
110
|
+
sanitized_map: dict[str, Any] = {}
|
|
111
|
+
for map_key, schema_item in item.items():
|
|
112
|
+
keep, sanitized_item = _sanitize_nim_schema_node(schema_item)
|
|
113
|
+
if keep:
|
|
114
|
+
sanitized_map[map_key] = sanitized_item
|
|
115
|
+
sanitized[key] = sanitized_map
|
|
116
|
+
else:
|
|
117
|
+
sanitized[key] = item
|
|
118
|
+
return True, sanitized
|
|
119
|
+
if isinstance(value, list):
|
|
120
|
+
sanitized_items = []
|
|
121
|
+
for item in value:
|
|
122
|
+
keep, sanitized_item = _sanitize_nim_schema_node(item)
|
|
123
|
+
if keep:
|
|
124
|
+
sanitized_items.append(sanitized_item)
|
|
125
|
+
return True, sanitized_items
|
|
126
|
+
return True, value
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _needs_nim_tool_parameter_alias(name: str) -> bool:
|
|
130
|
+
return name in _NIM_UNSAFE_TOOL_PARAMETER_NAMES
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _make_nim_tool_parameter_alias(name: str, reserved: set[str]) -> str:
|
|
134
|
+
safe_tail = "".join(
|
|
135
|
+
character if character.isalnum() or character == "_" else "_"
|
|
136
|
+
for character in name
|
|
137
|
+
).strip("_")
|
|
138
|
+
if not safe_tail:
|
|
139
|
+
safe_tail = "arg"
|
|
140
|
+
candidate = f"{_NIM_TOOL_PARAMETER_ALIAS_PREFIX}{safe_tail}"
|
|
141
|
+
alias = candidate
|
|
142
|
+
suffix = 2
|
|
143
|
+
while alias in reserved:
|
|
144
|
+
alias = f"{candidate}_{suffix}"
|
|
145
|
+
suffix += 1
|
|
146
|
+
reserved.add(alias)
|
|
147
|
+
return alias
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _collect_nim_tool_property_names(value: Any) -> set[str]:
|
|
151
|
+
names: set[str] = set()
|
|
152
|
+
if isinstance(value, dict):
|
|
153
|
+
properties = value.get("properties")
|
|
154
|
+
if isinstance(properties, dict):
|
|
155
|
+
for property_name, property_schema in properties.items():
|
|
156
|
+
if isinstance(property_name, str):
|
|
157
|
+
names.add(property_name)
|
|
158
|
+
names.update(_collect_nim_tool_property_names(property_schema))
|
|
159
|
+
for key, item in value.items():
|
|
160
|
+
if key != "properties":
|
|
161
|
+
names.update(_collect_nim_tool_property_names(item))
|
|
162
|
+
elif isinstance(value, list):
|
|
163
|
+
for item in value:
|
|
164
|
+
names.update(_collect_nim_tool_property_names(item))
|
|
165
|
+
return names
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _alias_nim_schema_property_names(
|
|
169
|
+
value: Any,
|
|
170
|
+
*,
|
|
171
|
+
reserved: set[str],
|
|
172
|
+
alias_to_original: dict[str, str],
|
|
173
|
+
original_to_alias: dict[str, str],
|
|
174
|
+
) -> Any:
|
|
175
|
+
if isinstance(value, list):
|
|
176
|
+
return [
|
|
177
|
+
_alias_nim_schema_property_names(
|
|
178
|
+
item,
|
|
179
|
+
reserved=reserved,
|
|
180
|
+
alias_to_original=alias_to_original,
|
|
181
|
+
original_to_alias=original_to_alias,
|
|
182
|
+
)
|
|
183
|
+
for item in value
|
|
184
|
+
]
|
|
185
|
+
if not isinstance(value, dict):
|
|
186
|
+
return value
|
|
187
|
+
|
|
188
|
+
local_aliases: dict[str, str] = {}
|
|
189
|
+
aliased_value: dict[str, Any] = {}
|
|
190
|
+
properties = value.get("properties")
|
|
191
|
+
if isinstance(properties, dict):
|
|
192
|
+
aliased_properties: dict[str, Any] = {}
|
|
193
|
+
for property_name, property_schema in properties.items():
|
|
194
|
+
aliased_schema = _alias_nim_schema_property_names(
|
|
195
|
+
property_schema,
|
|
196
|
+
reserved=reserved,
|
|
197
|
+
alias_to_original=alias_to_original,
|
|
198
|
+
original_to_alias=original_to_alias,
|
|
199
|
+
)
|
|
200
|
+
if isinstance(property_name, str) and _needs_nim_tool_parameter_alias(
|
|
201
|
+
property_name
|
|
202
|
+
):
|
|
203
|
+
alias = original_to_alias.get(property_name)
|
|
204
|
+
if alias is None:
|
|
205
|
+
alias = _make_nim_tool_parameter_alias(property_name, reserved)
|
|
206
|
+
alias_to_original[alias] = property_name
|
|
207
|
+
original_to_alias[property_name] = alias
|
|
208
|
+
local_aliases[property_name] = alias
|
|
209
|
+
aliased_properties[alias] = aliased_schema
|
|
210
|
+
else:
|
|
211
|
+
aliased_properties[property_name] = aliased_schema
|
|
212
|
+
aliased_value["properties"] = aliased_properties
|
|
213
|
+
|
|
214
|
+
for key, item in value.items():
|
|
215
|
+
if key == "properties":
|
|
216
|
+
continue
|
|
217
|
+
if key == "required" and isinstance(item, list):
|
|
218
|
+
aliased_value[key] = [
|
|
219
|
+
local_aliases.get(required_item, required_item)
|
|
220
|
+
if isinstance(required_item, str)
|
|
221
|
+
else required_item
|
|
222
|
+
for required_item in item
|
|
223
|
+
]
|
|
224
|
+
continue
|
|
225
|
+
aliased_value[key] = _alias_nim_schema_property_names(
|
|
226
|
+
item,
|
|
227
|
+
reserved=reserved,
|
|
228
|
+
alias_to_original=alias_to_original,
|
|
229
|
+
original_to_alias=original_to_alias,
|
|
230
|
+
)
|
|
231
|
+
return aliased_value
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _alias_nim_tool_parameters(
|
|
235
|
+
parameters: dict[str, Any],
|
|
236
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
237
|
+
alias_to_original: dict[str, str] = {}
|
|
238
|
+
original_to_alias: dict[str, str] = {}
|
|
239
|
+
reserved = _collect_nim_tool_property_names(parameters)
|
|
240
|
+
aliased_parameters = _alias_nim_schema_property_names(
|
|
241
|
+
parameters,
|
|
242
|
+
reserved=reserved,
|
|
243
|
+
alias_to_original=alias_to_original,
|
|
244
|
+
original_to_alias=original_to_alias,
|
|
245
|
+
)
|
|
246
|
+
if not alias_to_original:
|
|
247
|
+
return parameters, {}
|
|
248
|
+
return aliased_parameters, alias_to_original
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _sanitize_nim_tool_schemas(body: dict[str, Any]) -> None:
|
|
252
|
+
"""Sanitize only tool parameter schemas, preserving tool calls/history."""
|
|
253
|
+
tools = body.get("tools")
|
|
254
|
+
if not isinstance(tools, list):
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
tool_argument_aliases: dict[str, dict[str, str]] = {}
|
|
258
|
+
sanitized_tools: list[Any] = []
|
|
259
|
+
for tool in tools:
|
|
260
|
+
if not isinstance(tool, dict):
|
|
261
|
+
sanitized_tools.append(tool)
|
|
262
|
+
continue
|
|
263
|
+
sanitized_tool = dict(tool)
|
|
264
|
+
function = tool.get("function")
|
|
265
|
+
if isinstance(function, dict):
|
|
266
|
+
sanitized_function = dict(function)
|
|
267
|
+
parameters = function.get("parameters")
|
|
268
|
+
if isinstance(parameters, dict):
|
|
269
|
+
_, sanitized_parameters = _sanitize_nim_schema_node(parameters)
|
|
270
|
+
sanitized_parameters, argument_aliases = _alias_nim_tool_parameters(
|
|
271
|
+
sanitized_parameters
|
|
272
|
+
)
|
|
273
|
+
sanitized_function["parameters"] = sanitized_parameters
|
|
274
|
+
tool_name = function.get("name")
|
|
275
|
+
if argument_aliases and isinstance(tool_name, str) and tool_name:
|
|
276
|
+
tool_argument_aliases[tool_name] = argument_aliases
|
|
277
|
+
sanitized_tool["function"] = sanitized_function
|
|
278
|
+
sanitized_tools.append(sanitized_tool)
|
|
279
|
+
|
|
280
|
+
body["tools"] = sanitized_tools
|
|
281
|
+
if tool_argument_aliases:
|
|
282
|
+
body[NIM_TOOL_ARGUMENT_ALIASES_KEY] = tool_argument_aliases
|
|
283
|
+
else:
|
|
284
|
+
body.pop(NIM_TOOL_ARGUMENT_ALIASES_KEY, None)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def nim_tool_argument_aliases_from_body(
|
|
288
|
+
body: dict[str, Any],
|
|
289
|
+
) -> dict[str, dict[str, str]]:
|
|
290
|
+
"""Return validated private NIM tool argument aliases from a built body."""
|
|
291
|
+
raw_aliases = body.get(NIM_TOOL_ARGUMENT_ALIASES_KEY)
|
|
292
|
+
if not isinstance(raw_aliases, dict):
|
|
293
|
+
return {}
|
|
294
|
+
|
|
295
|
+
aliases: dict[str, dict[str, str]] = {}
|
|
296
|
+
for tool_name, tool_aliases in raw_aliases.items():
|
|
297
|
+
if not isinstance(tool_name, str) or not isinstance(tool_aliases, dict):
|
|
298
|
+
continue
|
|
299
|
+
sanitized_aliases = {
|
|
300
|
+
alias: original
|
|
301
|
+
for alias, original in tool_aliases.items()
|
|
302
|
+
if isinstance(alias, str) and isinstance(original, str)
|
|
303
|
+
}
|
|
304
|
+
if sanitized_aliases:
|
|
305
|
+
aliases[tool_name] = sanitized_aliases
|
|
306
|
+
return aliases
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def body_without_nim_tool_argument_aliases(body: dict[str, Any]) -> dict[str, Any]:
|
|
310
|
+
"""Return a request body with private alias metadata stripped before upstream I/O."""
|
|
311
|
+
if NIM_TOOL_ARGUMENT_ALIASES_KEY not in body:
|
|
312
|
+
return body
|
|
313
|
+
upstream_body = dict(body)
|
|
314
|
+
upstream_body.pop(NIM_TOOL_ARGUMENT_ALIASES_KEY, None)
|
|
315
|
+
return upstream_body
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _set_extra(
|
|
319
|
+
extra_body: dict[str, Any], key: str, value: Any, ignore_value: Any = None
|
|
320
|
+
) -> None:
|
|
321
|
+
if key in extra_body:
|
|
322
|
+
return
|
|
323
|
+
if value is None:
|
|
324
|
+
return
|
|
325
|
+
if ignore_value is not None and value == ignore_value:
|
|
326
|
+
return
|
|
327
|
+
extra_body[key] = value
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def clone_body_without_reasoning_budget(body: dict[str, Any]) -> dict[str, Any] | None:
|
|
331
|
+
"""Clone a request body and strip only reasoning_budget fields."""
|
|
332
|
+
return _clone_strip_extra_body(body, _strip_reasoning_budget_fields)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def clone_body_without_chat_template(body: dict[str, Any]) -> dict[str, Any] | None:
|
|
336
|
+
"""Clone a request body and strip only chat_template."""
|
|
337
|
+
return _clone_strip_extra_body(body, _strip_chat_template_field)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def clone_body_without_reasoning_content(body: dict[str, Any]) -> dict[str, Any] | None:
|
|
341
|
+
"""Clone a request body and strip assistant message ``reasoning_content`` fields."""
|
|
342
|
+
cloned_body = deepcopy(body)
|
|
343
|
+
if not _strip_message_reasoning_content(cloned_body):
|
|
344
|
+
return None
|
|
345
|
+
return cloned_body
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def build_request_body(
|
|
349
|
+
request_data: Any, nim: NimSettings, *, thinking_enabled: bool
|
|
350
|
+
) -> dict:
|
|
351
|
+
"""Build OpenAI-format request body from Anthropic request."""
|
|
352
|
+
logger.debug(
|
|
353
|
+
"NIM_REQUEST: conversion start model={} msgs={}",
|
|
354
|
+
getattr(request_data, "model", "?"),
|
|
355
|
+
len(getattr(request_data, "messages", [])),
|
|
356
|
+
)
|
|
357
|
+
try:
|
|
358
|
+
body = build_base_request_body(
|
|
359
|
+
request_data,
|
|
360
|
+
reasoning_replay=ReasoningReplayMode.REASONING_CONTENT
|
|
361
|
+
if thinking_enabled
|
|
362
|
+
else ReasoningReplayMode.DISABLED,
|
|
363
|
+
)
|
|
364
|
+
except OpenAIConversionError as exc:
|
|
365
|
+
raise InvalidRequestError(str(exc)) from exc
|
|
366
|
+
|
|
367
|
+
_sanitize_nim_tool_schemas(body)
|
|
368
|
+
|
|
369
|
+
# NIM-specific max_tokens: cap against nim.max_tokens
|
|
370
|
+
max_tokens = body.get("max_tokens") or getattr(request_data, "max_tokens", None)
|
|
371
|
+
if max_tokens is None:
|
|
372
|
+
max_tokens = nim.max_tokens
|
|
373
|
+
elif nim.max_tokens:
|
|
374
|
+
max_tokens = min(max_tokens, nim.max_tokens)
|
|
375
|
+
set_if_not_none(body, "max_tokens", max_tokens)
|
|
376
|
+
|
|
377
|
+
# NIM-specific temperature/top_p: fall back to NIM defaults if request didn't set
|
|
378
|
+
if body.get("temperature") is None and nim.temperature is not None:
|
|
379
|
+
body["temperature"] = nim.temperature
|
|
380
|
+
if body.get("top_p") is None and nim.top_p is not None:
|
|
381
|
+
body["top_p"] = nim.top_p
|
|
382
|
+
|
|
383
|
+
# NIM-specific stop sequences fallback
|
|
384
|
+
if "stop" not in body and nim.stop:
|
|
385
|
+
body["stop"] = nim.stop
|
|
386
|
+
|
|
387
|
+
if nim.presence_penalty != 0.0:
|
|
388
|
+
body["presence_penalty"] = nim.presence_penalty
|
|
389
|
+
if nim.frequency_penalty != 0.0:
|
|
390
|
+
body["frequency_penalty"] = nim.frequency_penalty
|
|
391
|
+
if nim.seed is not None:
|
|
392
|
+
body["seed"] = nim.seed
|
|
393
|
+
|
|
394
|
+
body["parallel_tool_calls"] = nim.parallel_tool_calls
|
|
395
|
+
|
|
396
|
+
# Handle non-standard parameters via extra_body
|
|
397
|
+
extra_body: dict[str, Any] = {}
|
|
398
|
+
request_extra = getattr(request_data, "extra_body", None)
|
|
399
|
+
if request_extra:
|
|
400
|
+
extra_body.update(request_extra)
|
|
401
|
+
|
|
402
|
+
if thinking_enabled:
|
|
403
|
+
chat_template_kwargs = extra_body.setdefault(
|
|
404
|
+
"chat_template_kwargs", {"thinking": True, "enable_thinking": True}
|
|
405
|
+
)
|
|
406
|
+
if isinstance(chat_template_kwargs, dict):
|
|
407
|
+
chat_template_kwargs.setdefault("reasoning_budget", max_tokens)
|
|
408
|
+
|
|
409
|
+
req_top_k = getattr(request_data, "top_k", None)
|
|
410
|
+
top_k = req_top_k if req_top_k is not None else nim.top_k
|
|
411
|
+
_set_extra(extra_body, "top_k", top_k, ignore_value=-1)
|
|
412
|
+
_set_extra(extra_body, "min_p", nim.min_p, ignore_value=0.0)
|
|
413
|
+
_set_extra(
|
|
414
|
+
extra_body, "repetition_penalty", nim.repetition_penalty, ignore_value=1.0
|
|
415
|
+
)
|
|
416
|
+
_set_extra(extra_body, "min_tokens", nim.min_tokens, ignore_value=0)
|
|
417
|
+
_set_extra(extra_body, "chat_template", nim.chat_template)
|
|
418
|
+
_set_extra(extra_body, "request_id", nim.request_id)
|
|
419
|
+
_set_extra(extra_body, "ignore_eos", nim.ignore_eos)
|
|
420
|
+
|
|
421
|
+
if extra_body:
|
|
422
|
+
body["extra_body"] = extra_body
|
|
423
|
+
|
|
424
|
+
logger.debug(
|
|
425
|
+
"NIM_REQUEST: conversion done model={} msgs={} tools={}",
|
|
426
|
+
body.get("model"),
|
|
427
|
+
len(body.get("messages", [])),
|
|
428
|
+
len(body.get("tools", [])),
|
|
429
|
+
)
|
|
430
|
+
return body
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""NVIDIA NIM / Riva offline ASR for voice notes (provider-owned transport)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
# NVIDIA NIM Whisper model mapping: (function_id, language_code)
|
|
10
|
+
_NIM_ASR_MODEL_MAP: dict[str, tuple[str, str]] = {
|
|
11
|
+
"nvidia/parakeet-ctc-0.6b-zh-tw": ("8473f56d-51ef-473c-bb26-efd4f5def2bf", "zh-TW"),
|
|
12
|
+
"nvidia/parakeet-ctc-0.6b-zh-cn": ("9add5ef7-322e-47e0-ad7a-5653fb8d259b", "zh-CN"),
|
|
13
|
+
# function-id from NVIDIA NIM API docs (parakeet-ctc-0.6b-es).
|
|
14
|
+
"nvidia/parakeet-ctc-0.6b-es": ("a9eeee8f-b509-4712-b19d-194361fa5f31", "es-US"),
|
|
15
|
+
"nvidia/parakeet-ctc-0.6b-vi": ("f3dff2bb-99f9-403d-a5f1-f574a757deb0", "vi-VN"),
|
|
16
|
+
"nvidia/parakeet-ctc-1.1b-asr": ("1598d209-5e27-4d3c-8079-4751568b1081", "en-US"),
|
|
17
|
+
"nvidia/parakeet-ctc-0.6b-asr": ("d8dd4e9b-fbf5-4fb0-9dba-8cf436c8d965", "en-US"),
|
|
18
|
+
"nvidia/parakeet-1.1b-rnnt-multilingual-asr": (
|
|
19
|
+
"71203149-d3b7-4460-8231-1be2543a1fca",
|
|
20
|
+
"",
|
|
21
|
+
),
|
|
22
|
+
"openai/whisper-large-v3": ("b702f636-f60c-4a3d-a6f4-f3568c13bd7d", "multi"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_RIVA_SERVER = "grpc.nvcf.nvidia.com:443"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def transcribe_audio_file(
|
|
29
|
+
file_path: Path,
|
|
30
|
+
model: str,
|
|
31
|
+
*,
|
|
32
|
+
api_key: str,
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Transcribe audio using NVIDIA NIM / Riva gRPC (offline recognition).
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to encoded audio bytes readable by Riva.
|
|
38
|
+
model: Hugging Face-style NIM model id (see ``_NIM_ASR_MODEL_MAP``).
|
|
39
|
+
api_key: NVIDIA API key (Bearer token); must be non-empty.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Transcript text, or ``(no speech detected)`` when empty.
|
|
43
|
+
"""
|
|
44
|
+
key = (api_key or "").strip()
|
|
45
|
+
if not key:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"NVIDIA NIM transcription requires a non-empty nvidia_nim_api_key "
|
|
48
|
+
"(configure NVIDIA_NIM_API_KEY or pass api_key explicitly)."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
import riva.client
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"NVIDIA NIM transcription requires the voice extra. "
|
|
56
|
+
"Install with: uv sync --extra voice"
|
|
57
|
+
) from e
|
|
58
|
+
|
|
59
|
+
model_config = _NIM_ASR_MODEL_MAP.get(model)
|
|
60
|
+
if not model_config:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"No NVIDIA NIM config found for model: {model}. "
|
|
63
|
+
f"Supported models: {', '.join(_NIM_ASR_MODEL_MAP.keys())}"
|
|
64
|
+
)
|
|
65
|
+
function_id, language_code = model_config
|
|
66
|
+
|
|
67
|
+
auth = riva.client.Auth(
|
|
68
|
+
use_ssl=True,
|
|
69
|
+
uri=_RIVA_SERVER,
|
|
70
|
+
metadata_args=[
|
|
71
|
+
["function-id", function_id],
|
|
72
|
+
["authorization", f"Bearer {key}"],
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
asr_service = riva.client.ASRService(auth)
|
|
77
|
+
|
|
78
|
+
config = riva.client.RecognitionConfig(
|
|
79
|
+
language_code=language_code,
|
|
80
|
+
max_alternatives=1,
|
|
81
|
+
verbatim_transcripts=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
with open(file_path, "rb") as f:
|
|
85
|
+
data = f.read()
|
|
86
|
+
|
|
87
|
+
response = asr_service.offline_recognize(data, config)
|
|
88
|
+
|
|
89
|
+
transcript = ""
|
|
90
|
+
results = getattr(response, "results", None)
|
|
91
|
+
if results and results[0].alternatives:
|
|
92
|
+
transcript = results[0].alternatives[0].transcript
|
|
93
|
+
|
|
94
|
+
logger.debug(f"NIM transcription: {len(transcript)} chars")
|
|
95
|
+
return transcript or "(no speech detected)"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Ollama provider implementation."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from providers.base import ProviderConfig
|
|
6
|
+
from providers.defaults import OLLAMA_DEFAULT_BASE
|
|
7
|
+
from providers.model_listing import extract_ollama_model_ids
|
|
8
|
+
from providers.transports.anthropic_messages import AnthropicMessagesTransport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OllamaProvider(AnthropicMessagesTransport):
|
|
12
|
+
"""Ollama provider using native Anthropic Messages API."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: ProviderConfig):
|
|
15
|
+
super().__init__(
|
|
16
|
+
config,
|
|
17
|
+
provider_name="OLLAMA",
|
|
18
|
+
default_base_url=OLLAMA_DEFAULT_BASE,
|
|
19
|
+
)
|
|
20
|
+
self._api_key = config.api_key or "ollama"
|
|
21
|
+
|
|
22
|
+
async def _send_stream_request(self, body: dict) -> httpx.Response:
|
|
23
|
+
"""Create a streaming native Anthropic messages response."""
|
|
24
|
+
request = self._client.build_request(
|
|
25
|
+
"POST",
|
|
26
|
+
"/v1/messages",
|
|
27
|
+
json=body,
|
|
28
|
+
headers=self._request_headers(),
|
|
29
|
+
)
|
|
30
|
+
return await self._client.send(request, stream=True)
|
|
31
|
+
|
|
32
|
+
async def _send_model_list_request(self) -> httpx.Response:
|
|
33
|
+
"""Query Ollama's native local model-list endpoint."""
|
|
34
|
+
return await self._client.get(f"{self._base_url}/api/tags")
|
|
35
|
+
|
|
36
|
+
def _extract_model_ids_from_model_list_payload(
|
|
37
|
+
self, payload: object
|
|
38
|
+
) -> frozenset[str]:
|
|
39
|
+
return extract_ollama_model_ids(payload, provider_name=self._provider_name)
|