glaip-sdk 0.7.13__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.
glaip_sdk/agents/base.py CHANGED
@@ -54,10 +54,13 @@ from glaip_sdk.utils.resource_refs import is_uuid
54
54
 
55
55
  if TYPE_CHECKING:
56
56
  from glaip_sdk.client.schedules import AgentScheduleManager
57
+ from glaip_sdk.models import AgentResponse, Model
57
58
  from glaip_sdk.guardrails import GuardrailManager
58
- from glaip_sdk.models import AgentResponse
59
59
  from glaip_sdk.registry import AgentRegistry, MCPRegistry, ToolRegistry
60
60
 
61
+ # Import model validation utility
62
+ from glaip_sdk.models._validation import _validate_model
63
+
61
64
  logger = logging.getLogger(__name__)
62
65
 
63
66
  _AGENT_NOT_DEPLOYED_MSG = "Agent must be deployed before running. Call deploy() first."
@@ -99,11 +102,11 @@ class Agent:
99
102
  - instruction: Agent instruction text (required)
100
103
  - description: Agent description (default: "")
101
104
  - tools: List of tools (default: [])
105
+ - model: Optional model override (default: None)
102
106
  - agents: List of sub-agents (default: [])
103
107
  - mcps: List of MCPs (default: [])
104
108
  - timeout: Timeout in seconds (default: 300)
105
109
  - metadata: Optional metadata dict (default: None)
106
- - model: Optional model override (default: None)
107
110
  - framework: Agent framework (default: "langchain")
108
111
  - version: Agent version (default: "1.0.0")
109
112
  - agent_type: Agent type (default: "config")
@@ -130,7 +133,7 @@ class Agent:
130
133
  tools: list | None = None,
131
134
  agents: list | None = None,
132
135
  mcps: list | None = None,
133
- model: str | None = _UNSET, # type: ignore[assignment]
136
+ model: str | Model | None = _UNSET, # type: ignore[assignment]
134
137
  guardrail: GuardrailManager | None = None,
135
138
  _client: Any = None,
136
139
  **kwargs: Any,
@@ -148,7 +151,7 @@ class Agent:
148
151
  tools: List of tools (Tool classes, SDK Tool objects, or strings).
149
152
  agents: List of sub-agents (Agent classes, instances, or strings).
150
153
  mcps: List of MCPs.
151
- model: Model identifier.
154
+ model: Model identifier or Model configuration object.
152
155
  guardrail: The guardrail manager for content safety.
153
156
  _client: Internal client reference (set automatically).
154
157
 
@@ -176,7 +179,7 @@ class Agent:
176
179
  self._tools = tools
177
180
  self._agents = agents
178
181
  self._mcps = mcps
179
- self._model = model
182
+ self._model = self._validate_and_set_model(model)
180
183
  self._guardrail = guardrail
181
184
  self._language_model_id: str | None = None
182
185
  # Extract parameters from kwargs with _UNSET defaults
@@ -210,6 +213,30 @@ class Agent:
210
213
  stacklevel=2,
211
214
  )
212
215
 
216
+ def _validate_and_set_model(self, model: str | Any) -> str | Any:
217
+ """Validate and normalize model parameter.
218
+
219
+ Supports both string model identifiers and Model objects:
220
+ - String: Simple model identifier (e.g., "openai/gpt-4o" or OpenAI.GPT_4O)
221
+ - Model: Model object with credentials/hyperparameters for local execution
222
+
223
+ Args:
224
+ model: Model identifier (string) or Model object.
225
+
226
+ Returns:
227
+ Validated model (string or Model object).
228
+ """
229
+ if model is None or model is Agent._UNSET:
230
+ return model
231
+
232
+ from glaip_sdk.models import Model # noqa: PLC0415
233
+
234
+ if isinstance(model, str):
235
+ return _validate_model(model)
236
+ elif isinstance(model, Model):
237
+ return model
238
+ return model
239
+
213
240
  # ─────────────────────────────────────────────────────────────────
214
241
  # Properties (override in subclasses OR pass to __init__)
215
242
  # ─────────────────────────────────────────────────────────────────
@@ -354,11 +381,11 @@ class Agent:
354
381
  return None
355
382
 
356
383
  @property
357
- def model(self) -> str | None:
384
+ def model(self) -> str | Model | None:
358
385
  """Optional model override.
359
386
 
360
387
  Returns:
361
- Model identifier string or None to use default.
388
+ Model identifier string, Model object, or None to use default.
362
389
  """
363
390
  if self._model is not self._UNSET:
364
391
  return self._model
@@ -561,9 +588,14 @@ class Agent:
561
588
  "framework": self.framework,
562
589
  "version": self.version,
563
590
  "agent_type": self.agent_type,
564
- "model": self.model,
565
591
  }
566
592
 
593
+ if self.model:
594
+ if isinstance(self.model, str):
595
+ config["model"] = self.model
596
+ else:
597
+ config["model"] = self.model.id
598
+
567
599
  # Handle metadata (default to empty dict if None)
568
600
  config["metadata"] = self.metadata or {}
569
601
 
@@ -11,37 +11,30 @@ Authors:
11
11
  # Import from submodules
12
12
  from glaip_sdk.cli.commands.agents._common import ( # noqa: E402
13
13
  AGENT_NOT_FOUND_ERROR,
14
- _get_agent_for_update,
15
- _resolve_agent,
16
- agents_group,
17
14
  _coerce_mapping_candidate,
18
15
  _display_agent_details,
19
16
  _emit_verbose_guidance,
20
17
  _fetch_full_agent_details,
18
+ _get_agent_for_update,
21
19
  _get_agent_model_name,
22
20
  _get_language_model_display_name,
23
21
  _model_from_config,
24
22
  _prepare_agent_output,
23
+ _resolve_agent,
25
24
  _resolve_resources_by_name,
25
+ agents_group,
26
26
  console,
27
27
  )
28
28
  from glaip_sdk.cli.commands.agents.create import create # noqa: E402
29
29
  from glaip_sdk.cli.commands.agents.delete import delete # noqa: E402
30
30
  from glaip_sdk.cli.commands.agents.get import get # noqa: E402
31
31
  from glaip_sdk.cli.commands.agents.list import list_agents # noqa: E402
32
- from glaip_sdk.cli.commands.agents.run import run, _maybe_attach_transcript_toggle # noqa: E402
32
+ from glaip_sdk.cli.commands.agents.run import _maybe_attach_transcript_toggle, run # noqa: E402
33
33
  from glaip_sdk.cli.commands.agents.sync_langflow import sync_langflow # noqa: E402
34
34
  from glaip_sdk.cli.commands.agents.update import update # noqa: E402
35
35
 
36
36
  # Import core functions for test compatibility
37
37
  from glaip_sdk.cli.core.context import get_client # noqa: E402
38
- from glaip_sdk.cli.core.rendering import with_client_and_spinner # noqa: E402
39
-
40
- # Import display functions for test compatibility
41
- from glaip_sdk.cli.display import ( # noqa: E402
42
- handle_json_output,
43
- handle_rich_output,
44
- )
45
38
 
46
39
  # Import core output functions for test compatibility
47
40
  from glaip_sdk.cli.core.output import ( # noqa: E402
@@ -52,11 +45,20 @@ from glaip_sdk.cli.core.output import ( # noqa: E402
52
45
  # Import rendering functions for test compatibility
53
46
  from glaip_sdk.cli.core.rendering import ( # noqa: E402
54
47
  build_renderer,
48
+ with_client_and_spinner, # noqa: E402
55
49
  )
56
50
 
57
51
  # Import display functions for test compatibility
58
- from glaip_sdk.cli.display import ( # noqa: E402
52
+ # Import display functions for test compatibility
53
+ from glaip_sdk.cli.display import ( # noqa: E402 # noqa: E402
59
54
  display_agent_run_suggestions,
55
+ handle_json_output,
56
+ handle_rich_output,
57
+ )
58
+
59
+ # Import IO functions for test compatibility
60
+ from glaip_sdk.cli.io import ( # noqa: E402
61
+ fetch_raw_resource_details,
60
62
  )
61
63
 
62
64
  # Import rich helpers for test compatibility
@@ -70,11 +72,6 @@ from glaip_sdk.cli.transcript import ( # noqa: E402
70
72
  store_transcript_for_session,
71
73
  )
72
74
 
73
- # Import IO functions for test compatibility
74
- from glaip_sdk.cli.io import ( # noqa: E402
75
- fetch_raw_resource_details,
76
- )
77
-
78
75
  # Import utils for test compatibility
79
76
  from glaip_sdk.utils import ( # noqa: E402
80
77
  is_uuid,
@@ -19,8 +19,11 @@ from glaip_sdk.branding import (
19
19
  WARNING_STYLE,
20
20
  )
21
21
  from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
22
- from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS, DEFAULT_MODEL
23
22
  from glaip_sdk.cli.context import get_ctx_value
23
+ from glaip_sdk.cli.core.output import (
24
+ output_result,
25
+ )
26
+ from glaip_sdk.cli.core.rendering import spinner_context
24
27
  from glaip_sdk.cli.display import (
25
28
  build_resource_result_data,
26
29
  handle_json_output,
@@ -31,10 +34,8 @@ from glaip_sdk.cli.hints import in_slash_mode
31
34
  from glaip_sdk.cli.io import fetch_raw_resource_details
32
35
  from glaip_sdk.cli.resolution import resolve_resource_reference
33
36
  from glaip_sdk.cli.rich_helpers import markup_text
34
- from glaip_sdk.cli.core.output import (
35
- output_result,
36
- )
37
- from glaip_sdk.cli.core.rendering import spinner_context
37
+ from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS
38
+ from glaip_sdk.models.constants import DEFAULT_MODEL
38
39
  from glaip_sdk.icons import ICON_AGENT
39
40
  from glaip_sdk.utils import format_datetime, is_uuid
40
41
 
@@ -11,19 +11,19 @@ from typing import Any
11
11
  import click
12
12
 
13
13
  from glaip_sdk.cli.context import output_flags
14
+ from glaip_sdk.cli.core.context import get_client
14
15
  from glaip_sdk.cli.display import (
15
16
  display_agent_run_suggestions,
16
17
  display_creation_success,
17
18
  handle_json_output,
18
19
  handle_rich_output,
19
20
  )
20
- from glaip_sdk.cli.core.context import get_client
21
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
22
21
  from glaip_sdk.cli.validators import (
23
22
  validate_agent_instruction_cli as validate_agent_instruction,
24
- validate_agent_name_cli as validate_agent_name,
25
- validate_timeout_cli as validate_timeout,
26
23
  )
24
+ from glaip_sdk.cli.validators import validate_agent_name_cli as validate_agent_name
25
+ from glaip_sdk.cli.validators import validate_timeout_cli as validate_timeout
26
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
27
27
  from glaip_sdk.utils.validation import coerce_timeout
28
28
 
29
29
  from ._common import (
@@ -67,7 +67,11 @@ def _handle_creation_exception(ctx: Any, e: Exception) -> None:
67
67
  @click.option("--instruction", help="Agent instruction (prompt)")
68
68
  @click.option(
69
69
  "--model",
70
- help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
70
+ help=(
71
+ "Language model in 'provider/model' format "
72
+ "(e.g., openai/gpt-4o-mini, bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0). "
73
+ "Use 'aip models list' to see available models."
74
+ ),
71
75
  )
72
76
  @click.option("--tools", multiple=True, help="Tool names or IDs to attach")
73
77
  @click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
@@ -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