glaip-sdk 0.7.12__py3-none-any.whl → 0.7.14__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.
@@ -17,18 +17,35 @@ from dataclasses import dataclass
17
17
  from typing import Any
18
18
 
19
19
  from rich.text import Text
20
- from textual.app import App, ComposeResult
21
- from textual.binding import Binding
22
- from textual.containers import Container, Horizontal, Vertical
23
- from textual.coordinate import Coordinate
24
- from textual.reactive import ReactiveError
25
- from textual.screen import ModalScreen
26
- from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
20
+
21
+ try: # pragma: no cover - optional dependency
22
+ from textual.app import App, ComposeResult
23
+ from textual.binding import Binding
24
+ from textual.containers import Horizontal, Vertical
25
+ from textual.coordinate import Coordinate
26
+ from textual.reactive import ReactiveError
27
+ from textual.screen import ModalScreen
28
+ from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
29
+ except Exception: # pragma: no cover - optional dependency
30
+ App = None # type: ignore[assignment]
31
+ ComposeResult = None # type: ignore[assignment]
32
+ Binding = None # type: ignore[assignment]
33
+ Horizontal = None # type: ignore[assignment]
34
+ Vertical = None # type: ignore[assignment]
35
+ Coordinate = None # type: ignore[assignment]
36
+ ReactiveError = Exception # type: ignore[assignment, misc]
37
+ ModalScreen = object # type: ignore[assignment, misc]
38
+ DataTable = None # type: ignore[assignment]
39
+ Footer = None # type: ignore[assignment]
40
+ Header = None # type: ignore[assignment]
41
+ LoadingIndicator = None # type: ignore[assignment]
42
+ RichLog = None # type: ignore[assignment]
43
+ Static = None # type: ignore[assignment]
27
44
 
28
45
  from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
29
46
  from glaip_sdk.cli.slash.tui.context import TUIContext
30
47
  from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
31
- from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastHandlerMixin
48
+ from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastContainer, ToastHandlerMixin
32
49
 
33
50
  logger = logging.getLogger(__name__)
34
51
 
@@ -149,7 +166,7 @@ class RunDetailScreen(ToastHandlerMixin, ClipboardToastMixin, ModalScreen[None])
149
166
  RichLog(id="detail-events", wrap=False),
150
167
  )
151
168
  yield main_content
152
- yield Container(Toast(), id="toast-container")
169
+ yield ToastContainer(Toast(), id="toast-container")
153
170
  yield Footer()
154
171
 
155
172
  def on_mount(self) -> None:
@@ -369,7 +386,7 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
369
386
  def compose(self) -> ComposeResult:
370
387
  """Build layout."""
371
388
  yield Header()
372
- yield Container(Toast(), id="toast-container")
389
+ yield ToastContainer(Toast(), id="toast-container")
373
390
  table = DataTable(id=RUNS_TABLE_ID) # pragma: no cover - mocked in tests
374
391
  table.cursor_type = "row" # pragma: no cover - mocked in tests
375
392
  table.add_columns( # pragma: no cover - mocked in tests
@@ -478,6 +495,26 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
478
495
  """Track cursor position when DataTable selection changes."""
479
496
  self.cursor_index = getattr(event, "cursor_row", self.cursor_index)
480
497
 
498
+ def _handle_table_click(self, row: int | None) -> None:
499
+ if row is None:
500
+ return
501
+ table = self.query_one(RUNS_TABLE_SELECTOR, DataTable)
502
+ self.cursor_index = row
503
+ try:
504
+ table.cursor_coordinate = Coordinate(row, 0)
505
+ except Exception:
506
+ return
507
+ self.action_open_detail()
508
+
509
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: # pragma: no cover - UI hook
510
+ """Handle row selection event from DataTable."""
511
+ self._handle_table_click(getattr(event, "cursor_row", None))
512
+
513
+ def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: # pragma: no cover - UI hook
514
+ """Handle cell selection event from DataTable."""
515
+ row = getattr(event.coordinate, "row", None) if event.coordinate else None
516
+ self._handle_table_click(row)
517
+
481
518
  def action_page_left(self) -> None:
482
519
  """Navigate to the previous page."""
483
520
  if not self.current_page.has_prev:
@@ -10,6 +10,7 @@ from typing import Any, cast
10
10
 
11
11
  from rich.text import Text
12
12
  from textual.message import Message
13
+ from textual.widget import Widget
13
14
  from textual.widgets import Static
14
15
 
15
16
 
@@ -290,6 +291,19 @@ class ClipboardToastMixin:
290
291
  self._append_copy_fallback(text)
291
292
 
292
293
 
294
+ class ToastContainer(Widget):
295
+ """Simple wrapper for docking toast widgets without relying on containers.
296
+
297
+ This class exists to provide a lightweight widget wrapper for toast containers
298
+ that avoids direct dependency on Textual's Container class. It allows the toast
299
+ system to work consistently across different Textual versions and provides a
300
+ stable API for toast container composition.
301
+
302
+ Usage:
303
+ yield ToastContainer(Toast(), id="toast-container")
304
+ """
305
+
306
+
293
307
  class Toast(Static):
294
308
  """A Textual widget that displays toast notifications at the top-right of the screen.
295
309
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ # pylint: disable=duplicate-code
2
2
  """Agent client for AIP SDK.
3
3
 
4
4
  Authors:
@@ -9,6 +9,7 @@ Authors:
9
9
  import asyncio
10
10
  import json
11
11
  import logging
12
+ import warnings
12
13
  from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
13
14
  from contextlib import asynccontextmanager
14
15
  from os import PathLike
@@ -21,15 +22,15 @@ if TYPE_CHECKING:
21
22
 
22
23
  import httpx
23
24
  from glaip_sdk.agents import Agent
25
+ from glaip_sdk.client.agent_runs import AgentRunsClient
26
+ from glaip_sdk.client.base import BaseClient
27
+ from glaip_sdk.client.mcps import MCPClient
24
28
  from glaip_sdk.client.payloads.agent import (
25
29
  AgentCreateRequest,
26
30
  AgentListParams,
27
31
  AgentListResult,
28
32
  AgentUpdateRequest,
29
33
  )
30
- from glaip_sdk.client.agent_runs import AgentRunsClient
31
- from glaip_sdk.client.base import BaseClient
32
- from glaip_sdk.client.mcps import MCPClient
33
34
  from glaip_sdk.client.run_rendering import (
34
35
  AgentRunRenderingManager,
35
36
  compute_timeout_seconds,
@@ -42,10 +43,10 @@ from glaip_sdk.config.constants import (
42
43
  DEFAULT_AGENT_RUN_TIMEOUT,
43
44
  DEFAULT_AGENT_TYPE,
44
45
  DEFAULT_AGENT_VERSION,
45
- DEFAULT_MODEL,
46
46
  )
47
47
  from glaip_sdk.exceptions import NotFoundError, ValidationError
48
48
  from glaip_sdk.models import AgentResponse
49
+ from glaip_sdk.models.constants import DEFAULT_MODEL
49
50
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
50
51
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
51
52
  from glaip_sdk.utils.client_utils import (
@@ -255,13 +256,16 @@ class AgentClient(BaseClient):
255
256
  self,
256
257
  *,
257
258
  parent_client: BaseClient | None = None,
259
+ lm_cache_ttl: float = 3600.0,
258
260
  **kwargs: Any,
259
261
  ) -> None:
260
262
  """Initialize the agent client.
261
263
 
262
264
  Args:
263
- parent_client: Parent client to adopt session/config from
264
- **kwargs: Additional arguments for standalone initialization
265
+ parent_client: Parent client to adopt session/config from.
266
+ lm_cache_ttl: TTL for the language model list cache in seconds.
267
+ Defaults to 3600 (1 hour).
268
+ **kwargs: Additional arguments for standalone initialization.
265
269
  """
266
270
  super().__init__(parent_client=parent_client, **kwargs)
267
271
  self._renderer_manager = AgentRunRenderingManager(logger)
@@ -270,6 +274,117 @@ class AgentClient(BaseClient):
270
274
  self._runs_client: AgentRunsClient | None = None
271
275
  self._schedule_client: ScheduleClient | None = None
272
276
 
277
+ self._lm_cache: list[dict[str, Any]] | None = None
278
+ self._lm_cache_time: float = 0.0
279
+ self._lm_cache_ttl: float = lm_cache_ttl
280
+
281
+ def clear_language_model_cache(self) -> None:
282
+ """Invalidate the language model list cache.
283
+
284
+ Forces the next call to list_language_models() to fetch a fresh list
285
+ from the server.
286
+ """
287
+ self._lm_cache = None
288
+ self._lm_cache_time = 0.0
289
+ logger.debug("Language model cache invalidated.")
290
+
291
+ def _resolve_language_model_id(self, model_str: str | None) -> str | None:
292
+ """Resolve a friendly model name to a server language model ID.
293
+
294
+ Handles provider name mapping (e.g., 'deepinfra/model' → 'openai-compatible/model')
295
+ by checking both the original provider name and its driver equivalent.
296
+
297
+ Args:
298
+ model_str: The model string to resolve (e.g., 'openai/gpt-4o', 'deepinfra/Qwen3-30B').
299
+
300
+ Returns:
301
+ The resolved server model ID (UUID), or None if not found.
302
+
303
+ Examples:
304
+ >>> _resolve_language_model_id("openai/gpt-4o")
305
+ "uuid-1234-..."
306
+ >>> _resolve_language_model_id("deepinfra/Qwen3-30B") # Maps to openai-compatible
307
+ "uuid-5678-..."
308
+ """
309
+ if not model_str:
310
+ return None
311
+
312
+ # If resolution is explicitly disabled (e.g. in unit tests to avoid extra API calls), skip it
313
+ if getattr(self, "_skip_model_resolution", False):
314
+ return None
315
+
316
+ try:
317
+ models = self.list_language_models()
318
+
319
+ # Try exact match first
320
+ model_id = self._find_exact_model_match(model_str, models)
321
+ if model_id:
322
+ return model_id
323
+
324
+ # Try with provider-to-driver mapping
325
+ return self._try_resolve_with_driver_mapping(model_str, models)
326
+ except Exception:
327
+ pass
328
+
329
+ return None
330
+
331
+ def _find_exact_model_match(self, model_str: str, models: list[dict[str, Any]]) -> str | None:
332
+ """Find exact model match in models list.
333
+
334
+ Args:
335
+ model_str: Model string to match.
336
+ models: List of language model dictionaries from server.
337
+
338
+ Returns:
339
+ Model ID (UUID) if found, None otherwise.
340
+ """
341
+ for model_info in models:
342
+ provider = model_info.get("provider")
343
+ name = model_info.get("name")
344
+ if provider and name:
345
+ full_name = f"{provider}/{name}"
346
+ if full_name == model_str:
347
+ return model_info.get("id")
348
+ if name == model_str:
349
+ return model_info.get("id")
350
+ return None
351
+
352
+ def _try_resolve_with_driver_mapping(self, model_str: str, models: list[dict[str, Any]]) -> str | None:
353
+ """Try to resolve model using provider-to-driver mapping.
354
+
355
+ Maps provider names to their driver implementations (e.g., deepinfra → openai-compatible)
356
+ and searches the models list with the driver name.
357
+
358
+ Args:
359
+ model_str: Model string in provider/model format (e.g., "deepinfra/Qwen3-30B").
360
+ models: List of language model dictionaries from server.
361
+
362
+ Returns:
363
+ Model ID (UUID) if found, None otherwise.
364
+ """
365
+ if "/" not in model_str:
366
+ return None
367
+
368
+ from glaip_sdk.models._provider_mappings import get_driver # noqa: PLC0415
369
+
370
+ provider, model_name = model_str.split("/", 1)
371
+ driver = get_driver(provider)
372
+
373
+ # Only try with driver if it's different from provider
374
+ if driver == provider:
375
+ return None
376
+
377
+ driver_model_str = f"{driver}/{model_name}"
378
+ for model_info in models:
379
+ provider_field = model_info.get("provider")
380
+ name_field = model_info.get("name")
381
+ if provider_field and name_field:
382
+ full_name = f"{provider_field}/{name_field}"
383
+ if full_name == driver_model_str:
384
+ return model_info.get("id")
385
+
386
+ return None
387
+
273
388
  def list_agents(
274
389
  self,
275
390
  query: AgentListParams | None = None,
@@ -461,11 +576,18 @@ class AgentClient(BaseClient):
461
576
  Returns:
462
577
  Final text string.
463
578
  """
464
- from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
579
+ from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
580
+ finalize_render_manager,
581
+ )
465
582
 
466
583
  manager = self._get_renderer_manager()
467
584
  return finalize_render_manager(
468
- manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
585
+ manager,
586
+ renderer,
587
+ final_text,
588
+ stats_usage,
589
+ started_monotonic,
590
+ finished_monotonic,
469
591
  )
470
592
 
471
593
  def _get_tool_client(self) -> ToolClient:
@@ -769,10 +891,18 @@ class AgentClient(BaseClient):
769
891
  plural_label="MCPs",
770
892
  )
771
893
 
772
- def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
773
- """Create an agent using a fully prepared payload mapping."""
774
- known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
894
+ def _validate_agent_basics(self, known: dict[str, Any]) -> tuple[str, str]:
895
+ """Validate and extract basic agent fields.
896
+
897
+ Args:
898
+ known: Known fields dictionary.
899
+
900
+ Returns:
901
+ Tuple of (name, validated_instruction).
775
902
 
903
+ Raises:
904
+ ValueError: If name or instruction is empty/whitespace.
905
+ """
776
906
  name = known.pop("name", None)
777
907
  instruction = known.pop("instruction", None)
778
908
  if not name or not str(name).strip():
@@ -781,9 +911,20 @@ class AgentClient(BaseClient):
781
911
  raise ValueError("Agent instruction cannot be empty or whitespace")
782
912
 
783
913
  validated_instruction = validate_agent_instruction(str(instruction))
784
- _normalise_sequence_fields(known)
914
+ return str(name).strip(), validated_instruction
785
915
 
786
- resolved_model = known.pop("model", None) or DEFAULT_MODEL
916
+ def _resolve_all_resources(
917
+ self, known: dict[str, Any], extras: dict[str, Any]
918
+ ) -> tuple[list[str] | None, list[str] | None, list[str] | None]:
919
+ """Resolve all resource IDs (tools, agents, mcps).
920
+
921
+ Args:
922
+ known: Known fields dictionary.
923
+ extras: Extra fields dictionary.
924
+
925
+ Returns:
926
+ Tuple of (resolved_tools, resolved_agents, resolved_mcps).
927
+ """
787
928
  tool_refs = extras.pop("_tool_refs", None)
788
929
  agent_refs = extras.pop("_agent_refs", None)
789
930
  mcp_refs = extras.pop("_mcp_refs", None)
@@ -796,10 +937,56 @@ class AgentClient(BaseClient):
796
937
  resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
797
938
  resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
798
939
 
940
+ return resolved_tools, resolved_agents, resolved_mcps
941
+
942
+ def _process_model_fields(
943
+ self, resolved_model: Any, known: dict[str, Any]
944
+ ) -> tuple[str, str | None, str | None, str | None]:
945
+ """Process model fields and extract language model ID.
946
+
947
+ Args:
948
+ resolved_model: Resolved model (string or Model object).
949
+ known: Known fields dictionary.
950
+
951
+ Returns:
952
+ Tuple of (resolved_model_str, language_model_id, provider, model_name).
953
+ """
954
+ from glaip_sdk.models import Model # noqa: PLC0415
955
+
956
+ if isinstance(resolved_model, Model):
957
+ if resolved_model.credentials or resolved_model.hyperparameters or resolved_model.base_url:
958
+ warnings.warn(
959
+ "Model object contains local configuration (credentials, hyperparameters, or base_url) "
960
+ "which is ignored for remote deployment. These fields are only used for local execution.",
961
+ UserWarning,
962
+ stacklevel=2,
963
+ )
964
+ resolved_model = resolved_model.id
965
+
966
+ # Validate and normalize string models (handles bare name deprecation)
967
+ if isinstance(resolved_model, str):
968
+ from glaip_sdk.models._validation import _validate_model # noqa: PLC0415
969
+
970
+ resolved_model = _validate_model(resolved_model)
971
+
799
972
  language_model_id = known.pop("language_model_id", None)
973
+ if not language_model_id and isinstance(resolved_model, str):
974
+ language_model_id = self._resolve_language_model_id(resolved_model)
975
+
800
976
  provider = known.pop("provider", None)
801
977
  model_name = known.pop("model_name", None)
802
978
 
979
+ return resolved_model, language_model_id, provider, model_name
980
+
981
+ def _extract_agent_metadata(self, known: dict[str, Any]) -> tuple[str, str, str]:
982
+ """Extract agent type, framework, and version.
983
+
984
+ Args:
985
+ known: Known fields dictionary.
986
+
987
+ Returns:
988
+ Tuple of (agent_type, framework, version).
989
+ """
803
990
  agent_type_value = known.pop("agent_type", None)
804
991
  fallback_type_value = known.pop("type", None)
805
992
  if agent_type_value is None:
@@ -807,6 +994,26 @@ class AgentClient(BaseClient):
807
994
 
808
995
  framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
809
996
  version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
997
+
998
+ return agent_type_value, framework_value, version_value
999
+
1000
+ def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
1001
+ """Create an agent using a fully prepared payload mapping."""
1002
+ known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
1003
+
1004
+ # Validate and extract basic fields
1005
+ name, validated_instruction = self._validate_agent_basics(known)
1006
+ _normalise_sequence_fields(known)
1007
+
1008
+ # Resolve model and resources
1009
+ resolved_model = known.pop("model", None) or DEFAULT_MODEL
1010
+ resolved_tools, resolved_agents, resolved_mcps = self._resolve_all_resources(known, extras)
1011
+
1012
+ # Process model and language model ID
1013
+ resolved_model, language_model_id, provider, model_name = self._process_model_fields(resolved_model, known)
1014
+
1015
+ # Extract agent type, framework, version
1016
+ agent_type_value, framework_value, version_value = self._extract_agent_metadata(known)
810
1017
  account_id = known.pop("account_id", None)
811
1018
  description = known.pop("description", None)
812
1019
  metadata = _prepare_agent_metadata(known.pop("metadata", None))
@@ -819,7 +1026,7 @@ class AgentClient(BaseClient):
819
1026
  final_extras.setdefault("model", resolved_model)
820
1027
 
821
1028
  request = AgentCreateRequest(
822
- name=str(name).strip(),
1029
+ name=name,
823
1030
  instruction=validated_instruction,
824
1031
  model=resolved_model,
825
1032
  language_model_id=language_model_id,
@@ -897,6 +1104,29 @@ class AgentClient(BaseClient):
897
1104
  """Backward-compatible helper to create an agent from a configuration file."""
898
1105
  return self.create_agent(file=file_path, **overrides)
899
1106
 
1107
+ def _resolve_update_model_fields(self, known: dict[str, Any]) -> None:
1108
+ """Resolve model fields in-place for update payload if present.
1109
+
1110
+ If 'model' or 'language_model_id' keys exist in the known fields dict,
1111
+ this method resolves them into 'language_model_id', 'provider', and 'model_name'
1112
+ using the standard resolution logic, ensuring consistency with create_agent.
1113
+
1114
+ Args:
1115
+ known: The dictionary of known fields to check and update in-place.
1116
+ """
1117
+ if "model" in known or "language_model_id" in known:
1118
+ model_val = known.pop("model", None)
1119
+ r_model, r_id, r_prov, r_name = self._process_model_fields(model_val, known)
1120
+
1121
+ if r_model is not None:
1122
+ known["model"] = r_model
1123
+ if r_id is not None:
1124
+ known["language_model_id"] = r_id
1125
+ if r_prov is not None:
1126
+ known["provider"] = r_prov
1127
+ if r_name is not None:
1128
+ known["model_name"] = r_name
1129
+
900
1130
  def _update_agent_from_payload(
901
1131
  self,
902
1132
  agent_id: str,
@@ -922,6 +1152,8 @@ class AgentClient(BaseClient):
922
1152
  if mcps_value is not None:
923
1153
  mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
924
1154
 
1155
+ self._resolve_update_model_fields(known)
1156
+
925
1157
  request = AgentUpdateRequest(
926
1158
  name=known.pop("name", None),
927
1159
  instruction=known.pop("instruction", None),
glaip_sdk/client/base.py CHANGED
@@ -8,6 +8,7 @@ Authors:
8
8
 
9
9
  import logging
10
10
  import os
11
+ import time
11
12
  from collections.abc import Iterable, Mapping
12
13
  from typing import Any, NoReturn, Union
13
14
 
@@ -89,6 +90,11 @@ class BaseClient:
89
90
  client_log.info(f"Initializing client with API URL: {self.api_url}")
90
91
  self.http_client = self._build_client(timeout)
91
92
 
93
+ # Language model cache (shared by all clients)
94
+ self._lm_cache: list[dict[str, Any]] | None = None
95
+ self._lm_cache_time: float = 0.0
96
+ self._lm_cache_ttl: float = 300.0 # 5 minutes TTL
97
+
92
98
  def _build_client(self, timeout: float) -> httpx.Client:
93
99
  """Build HTTP client with configuration."""
94
100
  # For streaming operations, we need more generous read timeouts
@@ -481,6 +487,25 @@ class BaseClient:
481
487
  request_id=request_id,
482
488
  )
483
489
 
490
+ def list_language_models(self, force_refresh: bool = False) -> list[dict[str, Any]]:
491
+ """List available language models with TTL caching.
492
+
493
+ Args:
494
+ force_refresh: Whether to ignore cache and fetch fresh list.
495
+
496
+ Returns:
497
+ List of available language models.
498
+ """
499
+ now = time.monotonic()
500
+ if not force_refresh and self._lm_cache is not None:
501
+ if now - self._lm_cache_time < self._lm_cache_ttl:
502
+ return self._lm_cache
503
+
504
+ models = self._request("GET", "/language-models") or []
505
+ self._lm_cache = models
506
+ self._lm_cache_time = now
507
+ return models
508
+
484
509
  def close(self) -> None:
485
510
  """Close the HTTP client."""
486
511
  if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
glaip_sdk/client/main.py CHANGED
@@ -212,10 +212,6 @@ class Client(BaseClient):
212
212
  return self.mcps.get_mcp_tools_from_config(config)
213
213
 
214
214
  # Language Models
215
- def list_language_models(self) -> list[dict]:
216
- """List available language models."""
217
- data = self._request("GET", "/language-models")
218
- return data or []
219
215
 
220
216
  # ---- Timeout propagation ----
221
217
  @property
@@ -17,8 +17,8 @@ from glaip_sdk.config.constants import (
17
17
  DEFAULT_AGENT_PROVIDER,
18
18
  DEFAULT_AGENT_TYPE,
19
19
  DEFAULT_AGENT_VERSION,
20
- DEFAULT_MODEL,
21
20
  )
21
+ from glaip_sdk.models.constants import DEFAULT_MODEL
22
22
  from glaip_sdk.payload_schemas.agent import AgentImportOperation, get_import_field_plan
23
23
  from glaip_sdk.utils.client_utils import extract_ids
24
24
 
@@ -100,6 +100,11 @@ def resolve_language_model_fields(
100
100
  resolved_model = model_name or model or default_model
101
101
  resolved_provider = provider if provider is not None else default_provider
102
102
 
103
+ if resolved_model and isinstance(resolved_model, str) and "/" in resolved_model:
104
+ parts = resolved_model.split("/", 1)
105
+ resolved_provider = parts[0]
106
+ resolved_model = parts[1]
107
+
103
108
  result: dict[str, Any] = {}
104
109
  if resolved_model is not None:
105
110
  result["model_name"] = resolved_model
@@ -4,8 +4,28 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- # Default language model configuration
8
- DEFAULT_MODEL = "gpt-5-nano"
7
+ # Lazy import cache for DEFAULT_MODEL to avoid circular dependency
8
+ _DEFAULT_MODEL: str | None = None
9
+
10
+
11
+ def __getattr__(name: str) -> str:
12
+ """Lazy import DEFAULT_MODEL from models.constants to avoid circular dependency.
13
+
14
+ Note: Prefer importing DEFAULT_MODEL directly from glaip_sdk.models.constants
15
+ as it is the canonical source. This re-export exists for backward compatibility.
16
+ """
17
+ if name in ("DEFAULT_MODEL", "SDK_DEFAULT_MODEL"):
18
+ global _DEFAULT_MODEL
19
+ if _DEFAULT_MODEL is None:
20
+ from glaip_sdk.models.constants import ( # noqa: PLC0415
21
+ DEFAULT_MODEL as _MODEL,
22
+ )
23
+
24
+ _DEFAULT_MODEL = _MODEL
25
+ return _DEFAULT_MODEL
26
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
27
+
28
+
9
29
  DEFAULT_AGENT_RUN_TIMEOUT = 300
10
30
 
11
31
  # User agent and version
@@ -1,3 +1,4 @@
1
+ # pylint: disable=duplicate-code
1
2
  """Models package for AIP SDK.
2
3
 
3
4
  This package provides Pydantic models for API responses.
@@ -17,7 +18,7 @@ Authors:
17
18
 
18
19
  import warnings
19
20
 
20
- # Pure Pydantic models for API responses (no runtime methods)
21
+ from glaip_sdk.models._validation import _validate_model, convert_model_for_local_execution
21
22
  from glaip_sdk.models.agent import AgentResponse
22
23
  from glaip_sdk.models.agent_runs import (
23
24
  RunOutputChunk,
@@ -26,7 +27,22 @@ from glaip_sdk.models.agent_runs import (
26
27
  RunWithOutput,
27
28
  )
28
29
  from glaip_sdk.models.common import LanguageModelResponse, TTYRenderer
30
+
31
+ # Pure Pydantic models for API responses (no runtime methods)
32
+ # Import model constants and validation (absolute imports)
33
+ from glaip_sdk.models.constants import (
34
+ DEFAULT_MODEL,
35
+ Anthropic,
36
+ AzureOpenAI,
37
+ Bedrock,
38
+ DeepInfra,
39
+ DeepSeek,
40
+ Google,
41
+ ModelProvider,
42
+ OpenAI,
43
+ )
29
44
  from glaip_sdk.models.mcp import MCPResponse
45
+ from glaip_sdk.models.model import Model
30
46
 
31
47
  # Export schedule models
32
48
  from glaip_sdk.models.schedule import ( # noqa: F401
@@ -82,6 +98,19 @@ def __getattr__(name: str) -> type:
82
98
 
83
99
 
84
100
  __all__ = [
101
+ # Model constants and validation
102
+ "OpenAI",
103
+ "Anthropic",
104
+ "Google",
105
+ "AzureOpenAI",
106
+ "DeepInfra",
107
+ "DeepSeek",
108
+ "Bedrock",
109
+ "Model",
110
+ "ModelProvider",
111
+ "DEFAULT_MODEL",
112
+ "_validate_model",
113
+ "convert_model_for_local_execution",
85
114
  # Pure Pydantic response models (recommended for type hints)
86
115
  "AgentResponse",
87
116
  "ToolResponse",