remdb 0.3.133__py3-none-any.whl → 0.3.157__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.
Files changed (51) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +310 -0
  3. rem/agentic/context_builder.py +5 -3
  4. rem/agentic/mcp/tool_wrapper.py +48 -6
  5. rem/agentic/providers/phoenix.py +91 -21
  6. rem/agentic/providers/pydantic_ai.py +77 -43
  7. rem/api/deps.py +2 -2
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/server.py +2 -0
  10. rem/api/mcp_router/tools.py +90 -0
  11. rem/api/routers/auth.py +208 -4
  12. rem/api/routers/chat/streaming.py +77 -22
  13. rem/auth/__init__.py +13 -3
  14. rem/auth/middleware.py +66 -1
  15. rem/auth/providers/__init__.py +4 -1
  16. rem/auth/providers/email.py +215 -0
  17. rem/cli/commands/configure.py +3 -4
  18. rem/cli/commands/experiments.py +50 -49
  19. rem/cli/commands/session.py +336 -0
  20. rem/cli/dreaming.py +2 -2
  21. rem/cli/main.py +2 -0
  22. rem/models/core/experiment.py +4 -14
  23. rem/models/entities/__init__.py +4 -0
  24. rem/models/entities/ontology.py +1 -1
  25. rem/models/entities/ontology_config.py +1 -1
  26. rem/models/entities/subscriber.py +175 -0
  27. rem/models/entities/user.py +1 -0
  28. rem/schemas/agents/core/agent-builder.yaml +134 -0
  29. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  30. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  31. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  32. rem/services/__init__.py +3 -1
  33. rem/services/content/service.py +4 -3
  34. rem/services/email/__init__.py +10 -0
  35. rem/services/email/service.py +459 -0
  36. rem/services/email/templates.py +360 -0
  37. rem/services/postgres/README.md +38 -0
  38. rem/services/postgres/diff_service.py +19 -3
  39. rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
  40. rem/services/session/compression.py +113 -50
  41. rem/services/session/reload.py +14 -7
  42. rem/settings.py +191 -4
  43. rem/sql/migrations/002_install_models.sql +91 -91
  44. rem/sql/migrations/005_schema_update.sql +145 -0
  45. rem/utils/README.md +45 -0
  46. rem/utils/files.py +157 -1
  47. rem/utils/vision.py +1 -1
  48. {remdb-0.3.133.dist-info → remdb-0.3.157.dist-info}/METADATA +7 -5
  49. {remdb-0.3.133.dist-info → remdb-0.3.157.dist-info}/RECORD +51 -42
  50. {remdb-0.3.133.dist-info → remdb-0.3.157.dist-info}/WHEEL +0 -0
  51. {remdb-0.3.133.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
rem/auth/__init__.py CHANGED
@@ -1,26 +1,36 @@
1
1
  """
2
2
  REM Authentication Module.
3
3
 
4
- OAuth 2.1 compliant authentication with support for:
4
+ Authentication with support for:
5
+ - Email passwordless login (verification codes)
5
6
  - Google OAuth
6
7
  - Microsoft Entra ID (Azure AD) OIDC
7
8
  - Custom OIDC providers
8
9
 
9
10
  Design Pattern:
10
11
  - Provider-agnostic base classes
11
- - PKCE (Proof Key for Code Exchange) for all flows
12
+ - PKCE (Proof Key for Code Exchange) for OAuth flows
12
13
  - State parameter for CSRF protection
13
14
  - Nonce for ID token replay protection
14
15
  - Token validation with JWKS
15
- - Clean separation: providers/ for OAuth logic, middleware.py for FastAPI integration
16
+ - Clean separation: providers/ for auth logic, middleware.py for FastAPI integration
17
+
18
+ Email Auth Flow:
19
+ 1. POST /api/auth/email/send-code with {email}
20
+ 2. User receives code via email
21
+ 3. POST /api/auth/email/verify with {email, code}
22
+ 4. Session created, user authenticated
16
23
  """
17
24
 
18
25
  from .providers.base import OAuthProvider
26
+ from .providers.email import EmailAuthProvider, EmailAuthResult
19
27
  from .providers.google import GoogleOAuthProvider
20
28
  from .providers.microsoft import MicrosoftOAuthProvider
21
29
 
22
30
  __all__ = [
23
31
  "OAuthProvider",
32
+ "EmailAuthProvider",
33
+ "EmailAuthResult",
24
34
  "GoogleOAuthProvider",
25
35
  "MicrosoftOAuthProvider",
26
36
  ]
rem/auth/middleware.py CHANGED
@@ -6,6 +6,7 @@ Supports anonymous access with rate limiting when allow_anonymous=True.
6
6
  MCP endpoints are always protected unless explicitly disabled.
7
7
 
8
8
  Design Pattern:
9
+ - Check X-API-Key header first (if API key auth enabled)
9
10
  - Check session for user on protected paths
10
11
  - Check Bearer token for dev token (non-production only)
11
12
  - MCP paths always require authentication (protected service)
@@ -20,6 +21,12 @@ Access Modes (configured in settings.auth):
20
21
  - mcp_requires_auth=true (default): MCP always requires login regardless of allow_anonymous
21
22
  - mcp_requires_auth=false: MCP follows normal allow_anonymous rules (dev only)
22
23
 
24
+ API Key Authentication (configured in settings.api):
25
+ - api_key_enabled=true: Require X-API-Key header for protected endpoints
26
+ - api_key: The secret key to validate against
27
+ - Provides simple programmatic access without OAuth flow
28
+ - X-API-Key header takes precedence over session auth
29
+
23
30
  Dev Token Support (non-production only):
24
31
  - GET /api/auth/dev/token returns a Bearer token for test-user
25
32
  - Include as: Authorization: Bearer dev_<signature>
@@ -82,6 +89,39 @@ class AuthMiddleware(BaseHTTPMiddleware):
82
89
  self.mcp_requires_auth = mcp_requires_auth
83
90
  self.mcp_path = mcp_path
84
91
 
92
+ def _check_api_key(self, request: Request) -> dict | None:
93
+ """
94
+ Check for valid X-API-Key header.
95
+
96
+ Returns:
97
+ API key user dict if valid, None otherwise
98
+ """
99
+ # Only check if API key auth is enabled
100
+ if not settings.api.api_key_enabled:
101
+ return None
102
+
103
+ # Check for X-API-Key header
104
+ api_key = request.headers.get("x-api-key")
105
+ if not api_key:
106
+ return None
107
+
108
+ # Validate against configured API key
109
+ if settings.api.api_key and api_key == settings.api.api_key:
110
+ logger.debug("X-API-Key authenticated")
111
+ return {
112
+ "id": "api-key-user",
113
+ "email": "api@rem.local",
114
+ "name": "API Key User",
115
+ "provider": "api-key",
116
+ "tenant_id": "default",
117
+ "tier": "pro", # API key users get full access
118
+ "roles": ["user"],
119
+ }
120
+
121
+ # Invalid API key
122
+ logger.warning("Invalid X-API-Key provided")
123
+ return None
124
+
85
125
  def _check_dev_token(self, request: Request) -> dict | None:
86
126
  """
87
127
  Check for valid dev token in Authorization header (non-production only).
@@ -105,7 +145,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
105
145
  # Verify dev token
106
146
  from ..api.routers.dev import verify_dev_token
107
147
  if verify_dev_token(token):
108
- logger.debug(f"Dev token authenticated as test-user")
148
+ logger.debug("Dev token authenticated as test-user")
109
149
  return {
110
150
  "id": "test-user",
111
151
  "email": "test@rem.local",
@@ -142,6 +182,31 @@ class AuthMiddleware(BaseHTTPMiddleware):
142
182
  if not is_protected or is_excluded:
143
183
  return await call_next(request)
144
184
 
185
+ # Check for X-API-Key header first (if enabled)
186
+ api_key_user = self._check_api_key(request)
187
+ if api_key_user:
188
+ request.state.user = api_key_user
189
+ request.state.is_anonymous = False
190
+ return await call_next(request)
191
+
192
+ # If API key auth is enabled but no valid key provided, reject immediately
193
+ if settings.api.api_key_enabled:
194
+ # Check if X-API-Key header was provided but invalid
195
+ if request.headers.get("x-api-key"):
196
+ logger.warning(f"Invalid X-API-Key for: {path}")
197
+ return JSONResponse(
198
+ status_code=401,
199
+ content={"detail": "Invalid API key"},
200
+ headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
201
+ )
202
+ # No API key provided when required
203
+ logger.debug(f"Missing X-API-Key for: {path}")
204
+ return JSONResponse(
205
+ status_code=401,
206
+ content={"detail": "API key required. Include X-API-Key header."},
207
+ headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
208
+ )
209
+
145
210
  # Check for dev token (non-production only)
146
211
  dev_user = self._check_dev_token(request)
147
212
  if dev_user:
@@ -1,6 +1,7 @@
1
- """OAuth provider implementations."""
1
+ """Authentication provider implementations."""
2
2
 
3
3
  from .base import OAuthProvider, OAuthTokens, OAuthUserInfo
4
+ from .email import EmailAuthProvider, EmailAuthResult
4
5
  from .google import GoogleOAuthProvider
5
6
  from .microsoft import MicrosoftOAuthProvider
6
7
 
@@ -8,6 +9,8 @@ __all__ = [
8
9
  "OAuthProvider",
9
10
  "OAuthTokens",
10
11
  "OAuthUserInfo",
12
+ "EmailAuthProvider",
13
+ "EmailAuthResult",
11
14
  "GoogleOAuthProvider",
12
15
  "MicrosoftOAuthProvider",
13
16
  ]
@@ -0,0 +1,215 @@
1
+ """
2
+ Email Authentication Provider.
3
+
4
+ Passwordless authentication using email verification codes.
5
+ Unlike OAuth providers, this handles the full flow internally.
6
+
7
+ Flow:
8
+ 1. User requests login with email address
9
+ 2. System generates code, upserts user, sends email
10
+ 3. User enters code
11
+ 4. System verifies code and creates session
12
+
13
+ Design:
14
+ - Uses EmailService for sending codes
15
+ - Creates users with deterministic UUID from email hash
16
+ - Stores challenge in user metadata
17
+ - No external OAuth dependencies
18
+ """
19
+
20
+ from typing import TYPE_CHECKING
21
+ from pydantic import BaseModel, Field
22
+ from loguru import logger
23
+
24
+ from ...services.email import EmailService
25
+
26
+ if TYPE_CHECKING:
27
+ from ...services.postgres import PostgresService
28
+
29
+
30
+ class EmailAuthResult(BaseModel):
31
+ """Result of email authentication operations."""
32
+
33
+ success: bool = Field(description="Whether operation succeeded")
34
+ email: str = Field(description="Email address")
35
+ user_id: str | None = Field(default=None, description="User ID if authenticated")
36
+ error: str | None = Field(default=None, description="Error message if failed")
37
+ message: str | None = Field(default=None, description="User-friendly message")
38
+
39
+
40
+ class EmailAuthProvider:
41
+ """
42
+ Email-based passwordless authentication provider.
43
+
44
+ Handles the complete email login flow:
45
+ 1. send_code() - Generate and send verification code
46
+ 2. verify_code() - Verify code and return user info
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ email_service: EmailService | None = None,
52
+ template_kwargs: dict | None = None,
53
+ ):
54
+ """
55
+ Initialize EmailAuthProvider.
56
+
57
+ Args:
58
+ email_service: EmailService instance (creates new one if not provided)
59
+ template_kwargs: Customization for email templates (colors, branding, etc.)
60
+ """
61
+ self._email_service = email_service or EmailService()
62
+ self._template_kwargs = template_kwargs or {}
63
+
64
+ @property
65
+ def is_configured(self) -> bool:
66
+ """Check if email auth is properly configured."""
67
+ return self._email_service.is_configured
68
+
69
+ async def send_code(
70
+ self,
71
+ email: str,
72
+ db: "PostgresService",
73
+ tenant_id: str = "default",
74
+ ) -> EmailAuthResult:
75
+ """
76
+ Send a verification code to an email address.
77
+
78
+ Creates user if not exists (using deterministic UUID from email).
79
+ Stores code in user metadata.
80
+
81
+ Args:
82
+ email: Email address to send code to
83
+ db: PostgresService instance
84
+ tenant_id: Tenant identifier
85
+
86
+ Returns:
87
+ EmailAuthResult with success status
88
+ """
89
+ if not self.is_configured:
90
+ return EmailAuthResult(
91
+ success=False,
92
+ email=email,
93
+ error="Email service not configured",
94
+ message="Email login is not available. Please try another method.",
95
+ )
96
+
97
+ try:
98
+ result = await self._email_service.send_login_code(
99
+ email=email,
100
+ db=db,
101
+ tenant_id=tenant_id,
102
+ template_kwargs=self._template_kwargs,
103
+ )
104
+
105
+ if result["success"]:
106
+ return EmailAuthResult(
107
+ success=True,
108
+ email=email,
109
+ user_id=result["user_id"],
110
+ message=f"Verification code sent to {email}. Check your inbox.",
111
+ )
112
+ else:
113
+ return EmailAuthResult(
114
+ success=False,
115
+ email=email,
116
+ error=result.get("error", "Failed to send code"),
117
+ message="Failed to send verification code. Please try again.",
118
+ )
119
+
120
+ except Exception as e:
121
+ logger.error(f"Error sending login code: {e}")
122
+ return EmailAuthResult(
123
+ success=False,
124
+ email=email,
125
+ error=str(e),
126
+ message="An error occurred. Please try again.",
127
+ )
128
+
129
+ async def verify_code(
130
+ self,
131
+ email: str,
132
+ code: str,
133
+ db: "PostgresService",
134
+ tenant_id: str = "default",
135
+ ) -> EmailAuthResult:
136
+ """
137
+ Verify a login code and authenticate user.
138
+
139
+ Args:
140
+ email: Email address
141
+ code: 6-digit verification code
142
+ db: PostgresService instance
143
+ tenant_id: Tenant identifier
144
+
145
+ Returns:
146
+ EmailAuthResult with user_id if successful
147
+ """
148
+ try:
149
+ result = await self._email_service.verify_login_code(
150
+ email=email,
151
+ code=code,
152
+ db=db,
153
+ tenant_id=tenant_id,
154
+ )
155
+
156
+ if result["valid"]:
157
+ return EmailAuthResult(
158
+ success=True,
159
+ email=email,
160
+ user_id=result["user_id"],
161
+ message="Successfully authenticated!",
162
+ )
163
+ else:
164
+ error = result.get("error", "Invalid code")
165
+ # User-friendly error messages
166
+ if error == "Login code expired":
167
+ message = "Your code has expired. Please request a new one."
168
+ elif error == "Invalid login code":
169
+ message = "Invalid code. Please check and try again."
170
+ elif error == "No login code requested":
171
+ message = "No code was requested for this email. Please request a new code."
172
+ elif error == "User not found":
173
+ message = "Email not found. Please request a login code first."
174
+ else:
175
+ message = "Verification failed. Please try again."
176
+
177
+ return EmailAuthResult(
178
+ success=False,
179
+ email=email,
180
+ error=error,
181
+ message=message,
182
+ )
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error verifying login code: {e}")
186
+ return EmailAuthResult(
187
+ success=False,
188
+ email=email,
189
+ error=str(e),
190
+ message="An error occurred. Please try again.",
191
+ )
192
+
193
+ def get_user_dict(self, email: str, user_id: str) -> dict:
194
+ """
195
+ Create a user dict for session storage.
196
+
197
+ Compatible with OAuth user format for consistent session handling.
198
+
199
+ Args:
200
+ email: User's email
201
+ user_id: User's UUID
202
+
203
+ Returns:
204
+ User dict for session
205
+ """
206
+ return {
207
+ "id": user_id,
208
+ "email": email,
209
+ "email_verified": True, # Email is verified through code
210
+ "name": email.split("@")[0], # Use email prefix as name
211
+ "provider": "email",
212
+ "tenant_id": "default",
213
+ "tier": "free", # Email users start at free tier
214
+ "roles": ["user"],
215
+ }
@@ -110,7 +110,7 @@ def prompt_llm_config(use_defaults: bool = False) -> dict:
110
110
  config = {}
111
111
 
112
112
  # Default values
113
- default_model = "anthropic:claude-sonnet-4-5-20250929"
113
+ default_model = "openai:gpt-4.1"
114
114
  default_temperature = 0.5
115
115
 
116
116
  if use_defaults:
@@ -124,9 +124,9 @@ def prompt_llm_config(use_defaults: bool = False) -> dict:
124
124
  # Default model
125
125
  click.echo("\nDefault LLM model (format: provider:model-id)")
126
126
  click.echo("Examples:")
127
+ click.echo(" - openai:gpt-4.1")
127
128
  click.echo(" - anthropic:claude-sonnet-4-5-20250929")
128
- click.echo(" - openai:gpt-4o")
129
- click.echo(" - openai:gpt-4o-mini")
129
+ click.echo(" - openai:gpt-4.1-mini")
130
130
 
131
131
  config["default_model"] = click.prompt(
132
132
  "Default model", default=default_model
@@ -422,7 +422,6 @@ def configure_command(install: bool, claude_desktop: bool, show: bool, edit: boo
422
422
 
423
423
  try:
424
424
  import shutil
425
- from pathlib import Path
426
425
  from fastmcp.mcp_config import update_config_file, StdioMCPServer
427
426
 
428
427
  # Find Claude Desktop config path
@@ -125,19 +125,17 @@ def create(
125
125
  # Resolve base path: CLI arg > EXPERIMENTS_HOME env var > default "experiments"
126
126
  if base_path is None:
127
127
  base_path = os.getenv("EXPERIMENTS_HOME", "experiments")
128
- # Build dataset reference
128
+ # Build dataset reference (format auto-detected from file extension)
129
129
  if dataset_location == "git":
130
130
  dataset_ref = DatasetReference(
131
131
  location=DatasetLocation.GIT,
132
132
  path="ground-truth/dataset.csv",
133
- format="csv",
134
133
  description="Ground truth Q&A dataset for evaluation"
135
134
  )
136
135
  else: # s3 or hybrid
137
136
  dataset_ref = DatasetReference(
138
137
  location=DatasetLocation(dataset_location),
139
138
  path=f"s3://rem-experiments/{name}/datasets/ground_truth.parquet",
140
- format="parquet",
141
139
  schema_path="datasets/schema.yaml" if dataset_location == "hybrid" else None,
142
140
  description="Ground truth dataset for evaluation"
143
141
  )
@@ -915,58 +913,61 @@ def run(
915
913
  click.echo(f" Last error: {evaluator_load_error}")
916
914
  raise click.Abort()
917
915
 
918
- # Load dataset using Polars
919
- import polars as pl
916
+ # Validate evaluator credentials before running expensive agent tasks
917
+ if evaluator_fn is not None and not only_vibes:
918
+ from rem.agentic.providers.phoenix import validate_evaluator_credentials
919
+
920
+ click.echo("Validating evaluator credentials...")
921
+ is_valid, error_msg = validate_evaluator_credentials()
922
+ if not is_valid:
923
+ click.echo(click.style(f"\n⚠️ Evaluator validation failed: {error_msg}", fg="yellow"))
924
+ click.echo("\nOptions:")
925
+ click.echo(" 1. Fix the credentials issue and re-run")
926
+ click.echo(" 2. Run with --only-vibes to skip LLM evaluation")
927
+ click.echo(" 3. Use --evaluator-model to specify a different model")
928
+ raise click.Abort()
929
+ click.echo("✓ Evaluator credentials validated")
930
+
931
+ # Load dataset using read_dataframe utility (auto-detects format from extension)
932
+ from rem.utils.files import read_dataframe
920
933
 
921
934
  click.echo(f"Loading dataset: {list(config.datasets.keys())[0]}")
922
935
  dataset_ref = list(config.datasets.values())[0]
923
936
 
924
- if dataset_ref.location.value == "git":
925
- # Load from Git (local filesystem)
926
- dataset_path = Path(base_path) / name / dataset_ref.path
927
- if not dataset_path.exists():
928
- click.echo(f"Error: Dataset not found: {dataset_path}")
929
- raise click.Abort()
937
+ try:
938
+ if dataset_ref.location.value == "git":
939
+ # Load from Git (local filesystem)
940
+ dataset_path = Path(base_path) / name / dataset_ref.path
941
+ if not dataset_path.exists():
942
+ click.echo(f"Error: Dataset not found: {dataset_path}")
943
+ raise click.Abort()
930
944
 
931
- if dataset_ref.format == "csv":
932
- dataset_df = pl.read_csv(dataset_path)
933
- elif dataset_ref.format == "parquet":
934
- dataset_df = pl.read_parquet(dataset_path)
935
- elif dataset_ref.format == "jsonl":
936
- dataset_df = pl.read_ndjson(dataset_path)
937
- else:
938
- click.echo(f"Error: Format '{dataset_ref.format}' not yet supported")
939
- raise click.Abort()
940
- elif dataset_ref.location.value in ["s3", "hybrid"]:
941
- # Load from S3 using FS provider
942
- from rem.services.fs import FS
943
- from io import BytesIO
945
+ dataset_df = read_dataframe(dataset_path)
944
946
 
945
- fs = FS()
946
-
947
- try:
948
- if dataset_ref.format == "csv":
949
- content = fs.read(dataset_ref.path)
950
- dataset_df = pl.read_csv(BytesIO(content.encode() if isinstance(content, str) else content))
951
- elif dataset_ref.format == "parquet":
952
- content_bytes = fs.read(dataset_ref.path)
953
- dataset_df = pl.read_parquet(BytesIO(content_bytes if isinstance(content_bytes, bytes) else content_bytes.encode()))
954
- elif dataset_ref.format == "jsonl":
955
- content = fs.read(dataset_ref.path)
956
- dataset_df = pl.read_ndjson(BytesIO(content.encode() if isinstance(content, str) else content))
957
- else:
958
- click.echo(f"Error: Format '{dataset_ref.format}' not yet supported")
959
- raise click.Abort()
947
+ elif dataset_ref.location.value in ["s3", "hybrid"]:
948
+ # Load from S3 using FS provider
949
+ from rem.services.fs import FS
960
950
 
951
+ fs = FS()
952
+ content = fs.read(dataset_ref.path)
953
+ # Ensure we have bytes
954
+ if isinstance(content, str):
955
+ content = content.encode()
956
+ dataset_df = read_dataframe(content, filename=dataset_ref.path)
961
957
  click.echo(f"✓ Loaded dataset from S3")
962
- except Exception as e:
963
- logger.error(f"Failed to load dataset from S3: {e}")
964
- click.echo(f"Error: Could not load dataset from S3")
965
- click.echo(f" Path: {dataset_ref.path}")
966
- click.echo(f" Format: {dataset_ref.format}")
958
+
959
+ else:
960
+ click.echo(f"Error: Unknown dataset location: {dataset_ref.location.value}")
967
961
  raise click.Abort()
968
- else:
969
- click.echo(f"Error: Unknown dataset location: {dataset_ref.location.value}")
962
+
963
+ except ValueError as e:
964
+ # Unsupported format error from read_dataframe
965
+ click.echo(f"Error: {e}")
966
+ raise click.Abort()
967
+ except Exception as e:
968
+ logger.error(f"Failed to load dataset: {e}")
969
+ click.echo(f"Error: Could not load dataset")
970
+ click.echo(f" Path: {dataset_ref.path}")
970
971
  raise click.Abort()
971
972
 
972
973
  click.echo(f"✓ Loaded dataset: {len(dataset_df)} examples")
@@ -1286,7 +1287,7 @@ def prompt():
1286
1287
  @click.option("--system-prompt", "-s", required=True, help="System prompt text")
1287
1288
  @click.option("--description", "-d", help="Prompt description")
1288
1289
  @click.option("--model-provider", default="OPENAI", help="Model provider (OPENAI, ANTHROPIC)")
1289
- @click.option("--model-name", "-m", help="Model name (e.g., gpt-4o, claude-sonnet-4-5)")
1290
+ @click.option("--model-name", "-m", help="Model name (e.g., gpt-4.1, claude-sonnet-4-5)")
1290
1291
  @click.option("--type", "-t", "prompt_type", default="Agent", help="Prompt type (Agent or Evaluator)")
1291
1292
  def prompt_create(
1292
1293
  name: str,
@@ -1302,7 +1303,7 @@ def prompt_create(
1302
1303
  # Create agent prompt
1303
1304
  rem experiments prompt create hello-world \\
1304
1305
  --system-prompt "You are a helpful assistant." \\
1305
- --model-name gpt-4o
1306
+ --model-name gpt-4.1
1306
1307
 
1307
1308
  # Create evaluator prompt
1308
1309
  rem experiments prompt create correctness-evaluator \\
@@ -1320,7 +1321,7 @@ def prompt_create(
1320
1321
  try:
1321
1322
  # Set default model if not specified
1322
1323
  if not model_name:
1323
- model_name = "gpt-4o" if model_provider == "OPENAI" else "claude-sonnet-4-5-20250929"
1324
+ model_name = "gpt-4.1" if model_provider == "OPENAI" else "claude-sonnet-4-5-20250929"
1324
1325
 
1325
1326
  # Get config
1326
1327
  phoenix_client = PhoenixClient()