self-evolve-framework 1.4.0 → 1.6.0

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 (171) hide show
  1. package/package.json +1 -1
  2. package/template/rules/ponytail.mdc +98 -23
  3. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  4. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  5. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  6. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  7. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  8. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  9. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  10. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  11. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  150. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
  151. package/template/skills/ponytail/SKILL.md +0 -133
  152. package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +0 -44
  153. package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +0 -21
  154. package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +0 -91
  155. package/template/skills/ponytail/scripts/hooks/ponytail-config.js +0 -122
  156. package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +0 -94
  157. package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +0 -55
  158. package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +0 -68
  159. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +0 -21
  160. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +0 -12
  161. package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +0 -22
  162. package/template/skills/ponytail/scripts/mcp/README.md +0 -46
  163. package/template/skills/ponytail/scripts/mcp/index.js +0 -48
  164. package/template/skills/ponytail/scripts/mcp/instructions.js +0 -26
  165. package/template/skills/ponytail/scripts/mcp/package.json +0 -13
  166. package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +0 -22
  167. package/template/skills/ponytail-audit/SKILL.md +0 -41
  168. package/template/skills/ponytail-debt/SKILL.md +0 -44
  169. package/template/skills/ponytail-gain/SKILL.md +0 -50
  170. package/template/skills/ponytail-help/SKILL.md +0 -69
  171. package/template/skills/ponytail-review/SKILL.md +0 -57
@@ -0,0 +1,915 @@
1
+ """ReflACT Model backend — Azure OpenAI wrapper with token tracking.
2
+
3
+ Provides optimizer/target dual-deployment chat functions and a global
4
+ TokenTracker for per-stage cost accounting. Previously llm/azure_openai.py.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ from types import SimpleNamespace
14
+ from typing import Any
15
+ from openai import AzureOpenAI, OpenAI
16
+
17
+ # Sentinel value used as the api_version when the "openai_compatible"
18
+ # auth_mode is selected. Real Azure deployments never use this string,
19
+ # so it doubles as a marker for downstream type narrowing.
20
+ _OPENAI_COMPATIBLE_API_VERSION = "openai-compat"
21
+
22
+ # ── Configuration ─────────────────────────────────────────────────────────────
23
+
24
+ ENDPOINT = os.environ.get(
25
+ "AZURE_OPENAI_ENDPOINT",
26
+ "", # Set via env var or config: e.g. "https://your-resource.openai.azure.com/"
27
+ )
28
+ API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview")
29
+ API_KEY = os.environ.get(
30
+ "AZURE_OPENAI_API_KEY",
31
+ "",
32
+ )
33
+ AUTH_MODE = os.environ.get("AZURE_OPENAI_AUTH_MODE", "azure_cli").strip().lower()
34
+ AD_SCOPE = os.environ.get(
35
+ "AZURE_OPENAI_AD_SCOPE",
36
+ "https://cognitiveservices.azure.com/.default",
37
+ )
38
+ MANAGED_IDENTITY_CLIENT_ID = os.environ.get(
39
+ "AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID",
40
+ "",
41
+ ).strip()
42
+
43
+ OPTIMIZER_ENDPOINT = (
44
+ os.environ.get("OPTIMIZER_AZURE_OPENAI_ENDPOINT")
45
+ or os.environ.get("AZURE_OPENAI_OPTIMIZER_ENDPOINT")
46
+ or ENDPOINT
47
+ )
48
+ TARGET_ENDPOINT = (
49
+ os.environ.get("TARGET_AZURE_OPENAI_ENDPOINT")
50
+ or os.environ.get("AZURE_OPENAI_TARGET_ENDPOINT")
51
+ or ENDPOINT
52
+ )
53
+ OPTIMIZER_API_VERSION = (
54
+ os.environ.get("OPTIMIZER_AZURE_OPENAI_API_VERSION")
55
+ or os.environ.get("AZURE_OPENAI_OPTIMIZER_API_VERSION")
56
+ or API_VERSION
57
+ )
58
+ TARGET_API_VERSION = (
59
+ os.environ.get("TARGET_AZURE_OPENAI_API_VERSION")
60
+ or os.environ.get("AZURE_OPENAI_TARGET_API_VERSION")
61
+ or API_VERSION
62
+ )
63
+ OPTIMIZER_API_KEY = (
64
+ os.environ.get("OPTIMIZER_AZURE_OPENAI_API_KEY")
65
+ or os.environ.get("AZURE_OPENAI_OPTIMIZER_API_KEY")
66
+ or API_KEY
67
+ )
68
+ TARGET_API_KEY = (
69
+ os.environ.get("TARGET_AZURE_OPENAI_API_KEY")
70
+ or os.environ.get("AZURE_OPENAI_TARGET_API_KEY")
71
+ or API_KEY
72
+ )
73
+ OPTIMIZER_AUTH_MODE = (
74
+ os.environ.get("OPTIMIZER_AZURE_OPENAI_AUTH_MODE")
75
+ or os.environ.get("AZURE_OPENAI_OPTIMIZER_AUTH_MODE")
76
+ or AUTH_MODE
77
+ ).strip().lower()
78
+ TARGET_AUTH_MODE = (
79
+ os.environ.get("TARGET_AZURE_OPENAI_AUTH_MODE")
80
+ or os.environ.get("AZURE_OPENAI_TARGET_AUTH_MODE")
81
+ or AUTH_MODE
82
+ ).strip().lower()
83
+ OPTIMIZER_AD_SCOPE = (
84
+ os.environ.get("OPTIMIZER_AZURE_OPENAI_AD_SCOPE")
85
+ or os.environ.get("AZURE_OPENAI_OPTIMIZER_AD_SCOPE")
86
+ or AD_SCOPE
87
+ )
88
+ TARGET_AD_SCOPE = (
89
+ os.environ.get("TARGET_AZURE_OPENAI_AD_SCOPE")
90
+ or os.environ.get("AZURE_OPENAI_TARGET_AD_SCOPE")
91
+ or AD_SCOPE
92
+ )
93
+ OPTIMIZER_MANAGED_IDENTITY_CLIENT_ID = (
94
+ os.environ.get("OPTIMIZER_AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID")
95
+ or os.environ.get("AZURE_OPENAI_OPTIMIZER_MANAGED_IDENTITY_CLIENT_ID")
96
+ or MANAGED_IDENTITY_CLIENT_ID
97
+ ).strip()
98
+ TARGET_MANAGED_IDENTITY_CLIENT_ID = (
99
+ os.environ.get("TARGET_AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID")
100
+ or os.environ.get("AZURE_OPENAI_TARGET_MANAGED_IDENTITY_CLIENT_ID")
101
+ or MANAGED_IDENTITY_CLIENT_ID
102
+ ).strip()
103
+
104
+ OPTIMIZER_DEPLOYMENT = os.environ.get("OPTIMIZER_DEPLOYMENT", "gpt-4o")
105
+ TARGET_DEPLOYMENT = os.environ.get("TARGET_DEPLOYMENT", "gpt-4o")
106
+
107
+ REASONING_EFFORT: str | None = None
108
+
109
+ _AZ_CLI_TOKEN_CACHE: dict[str, dict[str, Any]] = {}
110
+
111
+ # Deployments that require Responses API
112
+ _RESPONSES_API_MODELS = {
113
+ "gpt-5.3-codex", "gpt-5.1-codex", "gpt-5.2-codex",
114
+ "gpt-5-codex", "codex-mini", "gpt-5.4-pro",
115
+ }
116
+
117
+
118
+ # ── Token Tracker ─────────────────────────────────────────────────────────────
119
+
120
+ class TokenTracker:
121
+ """Thread-safe per-stage token counter."""
122
+
123
+ def __init__(self) -> None:
124
+ self._lock = threading.Lock()
125
+ self._data: dict[str, dict] = {}
126
+
127
+ def record(
128
+ self, stage: str, prompt_tokens: int, completion_tokens: int,
129
+ ) -> None:
130
+ with self._lock:
131
+ if stage not in self._data:
132
+ self._data[stage] = {
133
+ "calls": 0,
134
+ "prompt_tokens": 0,
135
+ "completion_tokens": 0,
136
+ }
137
+ d = self._data[stage]
138
+ d["calls"] += 1
139
+ d["prompt_tokens"] += prompt_tokens
140
+ d["completion_tokens"] += completion_tokens
141
+
142
+ def summary(self) -> dict:
143
+ with self._lock:
144
+ out: dict = {}
145
+ total_p = total_c = total_calls = 0
146
+ for stage, d in sorted(self._data.items()):
147
+ out[stage] = {
148
+ "calls": d["calls"],
149
+ "prompt_tokens": d["prompt_tokens"],
150
+ "completion_tokens": d["completion_tokens"],
151
+ "total_tokens": d["prompt_tokens"] + d["completion_tokens"],
152
+ }
153
+ total_p += d["prompt_tokens"]
154
+ total_c += d["completion_tokens"]
155
+ total_calls += d["calls"]
156
+ out["_total"] = {
157
+ "calls": total_calls,
158
+ "prompt_tokens": total_p,
159
+ "completion_tokens": total_c,
160
+ "total_tokens": total_p + total_c,
161
+ }
162
+ return out
163
+
164
+ def reset(self) -> None:
165
+ with self._lock:
166
+ self._data.clear()
167
+
168
+ def stage_snapshot(self, stage: str) -> dict:
169
+ """Return a copy of one stage's counters (or zeros if not tracked yet)."""
170
+ with self._lock:
171
+ d = self._data.get(stage, {})
172
+ return {
173
+ "calls": d.get("calls", 0),
174
+ "prompt_tokens": d.get("prompt_tokens", 0),
175
+ "completion_tokens": d.get("completion_tokens", 0),
176
+ "total_tokens": d.get("prompt_tokens", 0) + d.get("completion_tokens", 0),
177
+ }
178
+
179
+
180
+ tracker = TokenTracker()
181
+
182
+
183
+ # ── Client management ─────────────────────────────────────────────────────────
184
+
185
+ _optimizer_client: AzureOpenAI | OpenAI | None = None
186
+ _target_client: AzureOpenAI | OpenAI | None = None
187
+ _optimizer_lock = threading.Lock()
188
+ _target_lock = threading.Lock()
189
+
190
+
191
+ def _role_config(role: str) -> dict[str, str]:
192
+ if role == "optimizer":
193
+ return {
194
+ "endpoint": OPTIMIZER_ENDPOINT,
195
+ "api_version": OPTIMIZER_API_VERSION,
196
+ "api_key": OPTIMIZER_API_KEY,
197
+ "auth_mode": OPTIMIZER_AUTH_MODE,
198
+ "ad_scope": OPTIMIZER_AD_SCOPE,
199
+ "managed_identity_client_id": OPTIMIZER_MANAGED_IDENTITY_CLIENT_ID,
200
+ }
201
+ if role == "target":
202
+ return {
203
+ "endpoint": TARGET_ENDPOINT,
204
+ "api_version": TARGET_API_VERSION,
205
+ "api_key": TARGET_API_KEY,
206
+ "auth_mode": TARGET_AUTH_MODE,
207
+ "ad_scope": TARGET_AD_SCOPE,
208
+ "managed_identity_client_id": TARGET_MANAGED_IDENTITY_CLIENT_ID,
209
+ }
210
+ raise ValueError(f"Unknown Azure OpenAI client role: {role!r}")
211
+
212
+
213
+ def _make_token_provider(
214
+ auth_mode: str,
215
+ ad_scope: str,
216
+ managed_identity_client_id: str,
217
+ ):
218
+ try:
219
+ from azure.identity import ( # type: ignore[import-not-found]
220
+ AzureCliCredential,
221
+ ManagedIdentityCredential,
222
+ get_bearer_token_provider,
223
+ )
224
+ except ImportError as e:
225
+ if auth_mode == "azure_cli":
226
+ return _make_azure_cli_token_provider(ad_scope)
227
+ raise ImportError(
228
+ "Azure AD auth requires azure-identity. Install it with `pip install azure-identity`."
229
+ ) from e
230
+
231
+ if auth_mode in {"managed_identity", "aad", "azure_ad"}:
232
+ if managed_identity_client_id:
233
+ credential = ManagedIdentityCredential(client_id=managed_identity_client_id)
234
+ else:
235
+ credential = ManagedIdentityCredential()
236
+ elif auth_mode == "azure_cli":
237
+ credential = AzureCliCredential()
238
+ else:
239
+ raise ValueError(
240
+ "Unsupported Azure OpenAI auth mode "
241
+ f"{auth_mode!r}; expected api_key, managed_identity, azure_ad, aad, or azure_cli."
242
+ )
243
+ return get_bearer_token_provider(credential, ad_scope)
244
+
245
+
246
+ def _make_azure_cli_token_provider(ad_scope: str):
247
+ """Return an Azure CLI token provider compatible with AzureOpenAI.
248
+
249
+ This fallback avoids requiring azure-identity in environments where `az`
250
+ is already logged in. The SDK calls this provider whenever it needs a
251
+ bearer token.
252
+ """
253
+
254
+ resource = ad_scope.removesuffix("/.default")
255
+
256
+ def _provider() -> str:
257
+ now = int(time.time())
258
+ cache = _AZ_CLI_TOKEN_CACHE.setdefault(resource, {"token": "", "expires_on": 0})
259
+ cached = str(cache.get("token") or "")
260
+ expires_on = int(cache.get("expires_on") or 0)
261
+ if cached and expires_on - now > 300:
262
+ return cached
263
+
264
+ raw = subprocess.check_output(
265
+ [
266
+ "az",
267
+ "account",
268
+ "get-access-token",
269
+ "--resource",
270
+ resource,
271
+ "-o",
272
+ "json",
273
+ ],
274
+ text=True,
275
+ stderr=subprocess.STDOUT,
276
+ )
277
+ payload = json.loads(raw)
278
+ token = str(payload["accessToken"])
279
+ cache["token"] = token
280
+ cache["expires_on"] = int(payload.get("expires_on") or now + 3000)
281
+ return token
282
+
283
+ return _provider
284
+
285
+
286
+ def _make_client(role: str) -> AzureOpenAI | OpenAI:
287
+ cfg = _role_config(role)
288
+ if not cfg["endpoint"]:
289
+ raise ValueError(
290
+ f"Azure OpenAI endpoint is not configured for {role}. "
291
+ "Pass --azure_openai_endpoint https://your-resource.openai.azure.com/ "
292
+ "or set AZURE_OPENAI_ENDPOINT in your environment."
293
+ )
294
+ auth_mode = cfg["auth_mode"]
295
+ if auth_mode in {"openai_compatible", "compat", "openai"}:
296
+ return OpenAI(
297
+ base_url=cfg["endpoint"].rstrip("/"),
298
+ api_key=cfg["api_key"] or "dummy",
299
+ default_headers={"User-Agent": "SkillOpt"},
300
+ )
301
+ if auth_mode in {"api_key", "key"}:
302
+ if not cfg["api_key"]:
303
+ raise ValueError(
304
+ f"Azure OpenAI API key is not configured for {role}. "
305
+ "Set model.azure_openai_api_key in the config or export AZURE_OPENAI_API_KEY."
306
+ )
307
+ return AzureOpenAI(
308
+ api_version=cfg["api_version"],
309
+ azure_endpoint=cfg["endpoint"],
310
+ api_key=cfg["api_key"],
311
+ )
312
+ return AzureOpenAI(
313
+ api_version=cfg["api_version"],
314
+ azure_endpoint=cfg["endpoint"],
315
+ azure_ad_token_provider=_make_token_provider(
316
+ auth_mode,
317
+ cfg["ad_scope"],
318
+ cfg["managed_identity_client_id"],
319
+ ),
320
+ )
321
+
322
+
323
+ def get_optimizer_client() -> AzureOpenAI | OpenAI:
324
+ global _optimizer_client
325
+ with _optimizer_lock:
326
+ if _optimizer_client is None:
327
+ _optimizer_client = _make_client("optimizer")
328
+ return _optimizer_client
329
+
330
+
331
+ def get_target_client() -> AzureOpenAI | OpenAI:
332
+ global _target_client
333
+ with _target_lock:
334
+ if _target_client is None:
335
+ # When using qwen_chat backend, return an OpenAI client pointing to vLLM
336
+ from skillopt.model.backend_config import get_target_backend
337
+ if get_target_backend() == "qwen_chat":
338
+ from skillopt.model import qwen_backend as _qwen
339
+ target_config = _qwen.TARGET_CONFIG
340
+ _target_client = OpenAI(
341
+ base_url=target_config.base_url,
342
+ api_key=target_config.api_key or "dummy",
343
+ )
344
+ else:
345
+ _target_client = _make_client("target")
346
+ return _target_client
347
+
348
+
349
+ def _needs_responses_api(deployment: str) -> bool:
350
+ dep = deployment.lower()
351
+ return any(dep == m or dep.startswith(m + "-") for m in _RESPONSES_API_MODELS)
352
+
353
+
354
+ # ── Core chat function ────────────────────────────────────────────────────────
355
+
356
+ def _chat_impl(
357
+ client: AzureOpenAI | OpenAI,
358
+ deployment: str,
359
+ system: str,
360
+ user: str,
361
+ max_completion_tokens: int,
362
+ retries: int,
363
+ stage: str,
364
+ reasoning_effort: str | None = None,
365
+ timeout: int | None = None,
366
+ ) -> tuple[str, dict]:
367
+ """Call LLM, track tokens, return (text, usage_dict)."""
368
+ last_err = None
369
+ usage_info = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
370
+
371
+ for attempt in range(retries):
372
+ try:
373
+ if _needs_responses_api(deployment):
374
+ kwargs: dict[str, Any] = {
375
+ "model": deployment,
376
+ "instructions": system,
377
+ "input": [{"role": "user", "content": user}],
378
+ "max_output_tokens": max_completion_tokens,
379
+ }
380
+ actual_effort = reasoning_effort or REASONING_EFFORT
381
+ if actual_effort:
382
+ kwargs["reasoning"] = {"effort": actual_effort}
383
+ if timeout is not None:
384
+ kwargs["timeout"] = timeout
385
+ resp = client.responses.create(**kwargs)
386
+ text = getattr(resp, "output_text", None) or ""
387
+ if not text:
388
+ for item in getattr(resp, "output", None) or []:
389
+ for part in getattr(item, "content", []):
390
+ if getattr(part, "type", "") == "output_text":
391
+ text = part.text or ""
392
+ if hasattr(resp, "usage") and resp.usage:
393
+ usage_info = {
394
+ "prompt_tokens": getattr(resp.usage, "input_tokens", 0) or 0,
395
+ "completion_tokens": getattr(resp.usage, "output_tokens", 0) or 0,
396
+ "total_tokens": (
397
+ (getattr(resp.usage, "input_tokens", 0) or 0)
398
+ + (getattr(resp.usage, "output_tokens", 0) or 0)
399
+ ),
400
+ }
401
+ else:
402
+ kwargs: dict[str, Any] = dict(
403
+ model=deployment,
404
+ messages=[
405
+ {"role": "system", "content": system},
406
+ {"role": "user", "content": user},
407
+ ],
408
+ max_completion_tokens=max_completion_tokens,
409
+ )
410
+ actual_effort = reasoning_effort or REASONING_EFFORT
411
+ if actual_effort is not None:
412
+ kwargs["reasoning_effort"] = actual_effort
413
+ if timeout is not None:
414
+ kwargs["timeout"] = timeout
415
+ resp = client.chat.completions.create(**kwargs)
416
+ text = resp.choices[0].message.content or ""
417
+ if resp.usage:
418
+ usage_info = {
419
+ "prompt_tokens": resp.usage.prompt_tokens or 0,
420
+ "completion_tokens": resp.usage.completion_tokens or 0,
421
+ "total_tokens": resp.usage.total_tokens or 0,
422
+ }
423
+
424
+ tracker.record(
425
+ stage,
426
+ usage_info["prompt_tokens"],
427
+ usage_info["completion_tokens"],
428
+ )
429
+ return text, usage_info
430
+
431
+ except Exception as e: # noqa: BLE001
432
+ last_err = e
433
+ sleep = min(2 ** attempt, 30)
434
+ time.sleep(sleep)
435
+
436
+ raise RuntimeError(f"LLM call failed after {retries} retries: {last_err}")
437
+
438
+
439
+ def _chat_messages_impl(
440
+ client: AzureOpenAI | OpenAI,
441
+ deployment: str,
442
+ messages: list[dict[str, Any]],
443
+ max_completion_tokens: int,
444
+ retries: int,
445
+ stage: str,
446
+ reasoning_effort: str | None = None,
447
+ *,
448
+ tools: list[dict[str, Any]] | None = None,
449
+ tool_choice: str | dict[str, Any] | None = None,
450
+ return_message: bool = False,
451
+ timeout: int | None = None,
452
+ ) -> tuple[Any, dict]:
453
+ """Call the model with a pre-built message list."""
454
+ last_err = None
455
+ usage_info = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
456
+
457
+ for attempt in range(retries):
458
+ try:
459
+ if _needs_responses_api(deployment):
460
+ input_items, instructions = _messages_to_responses_input(messages)
461
+ kwargs: dict[str, Any] = {
462
+ "model": deployment,
463
+ "input": input_items,
464
+ "max_output_tokens": max_completion_tokens,
465
+ }
466
+ if instructions:
467
+ kwargs["instructions"] = instructions
468
+ actual_effort = reasoning_effort or REASONING_EFFORT
469
+ if actual_effort:
470
+ kwargs["reasoning"] = {"effort": actual_effort}
471
+ if tools:
472
+ kwargs["tools"] = [_chat_tool_to_responses_tool(tool) for tool in tools]
473
+ if tool_choice is not None:
474
+ kwargs["tool_choice"] = tool_choice
475
+ if timeout is not None:
476
+ kwargs["timeout"] = timeout
477
+ resp = client.responses.create(**kwargs)
478
+ message, text = _responses_to_chat_message(resp)
479
+ if hasattr(resp, "usage") and resp.usage:
480
+ usage_info = {
481
+ "prompt_tokens": getattr(resp.usage, "input_tokens", 0) or 0,
482
+ "completion_tokens": getattr(resp.usage, "output_tokens", 0) or 0,
483
+ "total_tokens": (
484
+ (getattr(resp.usage, "input_tokens", 0) or 0)
485
+ + (getattr(resp.usage, "output_tokens", 0) or 0)
486
+ ),
487
+ }
488
+ else:
489
+ kwargs = dict(
490
+ model=deployment,
491
+ messages=messages,
492
+ max_completion_tokens=max_completion_tokens,
493
+ )
494
+ actual_effort = reasoning_effort or REASONING_EFFORT
495
+ if tools:
496
+ kwargs["tools"] = tools
497
+ if tool_choice is not None:
498
+ kwargs["tool_choice"] = tool_choice
499
+ # Some models (e.g. gpt-5.5) don't support reasoning_effort with function tools
500
+ elif actual_effort is not None:
501
+ kwargs["reasoning_effort"] = actual_effort
502
+ if timeout is not None:
503
+ kwargs["timeout"] = timeout
504
+ resp = client.chat.completions.create(**kwargs)
505
+ message = resp.choices[0].message
506
+ text = message.content or ""
507
+ if resp.usage:
508
+ usage_info = {
509
+ "prompt_tokens": resp.usage.prompt_tokens or 0,
510
+ "completion_tokens": resp.usage.completion_tokens or 0,
511
+ "total_tokens": resp.usage.total_tokens or 0,
512
+ }
513
+ tracker.record(
514
+ stage,
515
+ usage_info["prompt_tokens"],
516
+ usage_info["completion_tokens"],
517
+ )
518
+ return (message if return_message else text), usage_info
519
+ except Exception as e: # noqa: BLE001
520
+ last_err = e
521
+ sleep = min(2 ** attempt, 30)
522
+ time.sleep(sleep)
523
+
524
+ raise RuntimeError(f"LLM message call failed after {retries} retries: {last_err}")
525
+
526
+
527
+ def _chat_tool_to_responses_tool(tool: dict[str, Any]) -> dict[str, Any]:
528
+ """Convert a Chat Completions function tool to Responses API format."""
529
+ if tool.get("type") == "function" and isinstance(tool.get("function"), dict):
530
+ fn = tool["function"]
531
+ return {
532
+ "type": "function",
533
+ "name": fn.get("name", ""),
534
+ "description": fn.get("description", ""),
535
+ "parameters": fn.get("parameters", {"type": "object", "properties": {}}),
536
+ }
537
+ return tool
538
+
539
+
540
+ def _messages_to_responses_input(messages: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], str]:
541
+ """Convert chat-style messages, including tool results, to Responses input."""
542
+ instructions: list[str] = []
543
+ input_items: list[dict[str, Any]] = []
544
+ for message in messages:
545
+ role = message.get("role")
546
+ content = message.get("content") or ""
547
+ if role == "system":
548
+ if content:
549
+ instructions.append(str(content))
550
+ continue
551
+ if role == "tool":
552
+ input_items.append({
553
+ "type": "function_call_output",
554
+ "call_id": str(message.get("tool_call_id", "")),
555
+ "output": str(content),
556
+ })
557
+ continue
558
+ if role == "assistant":
559
+ if content:
560
+ input_items.append({"role": "assistant", "content": str(content)})
561
+ for tool_call in message.get("tool_calls") or []:
562
+ function = tool_call.get("function", {}) or {}
563
+ input_items.append({
564
+ "type": "function_call",
565
+ "call_id": str(tool_call.get("id", "")),
566
+ "name": str(function.get("name", "")),
567
+ "arguments": str(function.get("arguments", "{}") or "{}"),
568
+ })
569
+ continue
570
+ if role in {"user", "developer"}:
571
+ input_items.append({"role": "user", "content": str(content)})
572
+ return input_items, "\n\n".join(instructions)
573
+
574
+
575
+ def _responses_to_chat_message(resp: Any) -> tuple[Any, str]:
576
+ """Convert Responses output into the subset of Chat message API we use."""
577
+ text = getattr(resp, "output_text", None) or ""
578
+ tool_calls: list[dict[str, Any]] = []
579
+ for item in getattr(resp, "output", None) or []:
580
+ item_type = getattr(item, "type", "")
581
+ if item_type == "function_call":
582
+ tool_calls.append({
583
+ "id": getattr(item, "call_id", "") or getattr(item, "id", ""),
584
+ "type": "function",
585
+ "function": {
586
+ "name": getattr(item, "name", ""),
587
+ "arguments": getattr(item, "arguments", "") or "{}",
588
+ },
589
+ })
590
+ elif item_type == "message" and not text:
591
+ content_parts = getattr(item, "content", []) or []
592
+ for part in content_parts:
593
+ if getattr(part, "type", "") == "output_text":
594
+ text += getattr(part, "text", "") or ""
595
+ return SimpleNamespace(content=text, tool_calls=tool_calls), text
596
+
597
+
598
+ # ── Public API ────────────────────────────────────────────────────────────────
599
+
600
+ def configure_azure_openai(
601
+ *,
602
+ endpoint: str | None = None,
603
+ api_version: str | None = None,
604
+ api_key: str | None = None,
605
+ auth_mode: str | None = None,
606
+ ad_scope: str | None = None,
607
+ managed_identity_client_id: str | None = None,
608
+ optimizer_endpoint: str | None = None,
609
+ optimizer_api_version: str | None = None,
610
+ optimizer_api_key: str | None = None,
611
+ optimizer_auth_mode: str | None = None,
612
+ optimizer_ad_scope: str | None = None,
613
+ optimizer_managed_identity_client_id: str | None = None,
614
+ target_endpoint: str | None = None,
615
+ target_api_version: str | None = None,
616
+ target_api_key: str | None = None,
617
+ target_auth_mode: str | None = None,
618
+ target_ad_scope: str | None = None,
619
+ target_managed_identity_client_id: str | None = None,
620
+ ) -> None:
621
+ global ENDPOINT, API_VERSION, API_KEY, AUTH_MODE, AD_SCOPE, MANAGED_IDENTITY_CLIENT_ID
622
+ global OPTIMIZER_ENDPOINT, OPTIMIZER_API_VERSION, OPTIMIZER_API_KEY, OPTIMIZER_AUTH_MODE
623
+ global OPTIMIZER_AD_SCOPE, OPTIMIZER_MANAGED_IDENTITY_CLIENT_ID
624
+ global TARGET_ENDPOINT, TARGET_API_VERSION, TARGET_API_KEY, TARGET_AUTH_MODE
625
+ global TARGET_AD_SCOPE, TARGET_MANAGED_IDENTITY_CLIENT_ID
626
+ global _optimizer_client, _target_client
627
+
628
+ def _clean(value: str | None, *, lower: bool = False) -> str | None:
629
+ if value is None:
630
+ return None
631
+ str_value = str(value).strip()
632
+ if not str_value:
633
+ return None
634
+ if lower:
635
+ str_value = str_value.lower()
636
+ return str_value
637
+
638
+ def _set(global_name: str, value: str | None, env_key: str) -> None:
639
+ if value is None:
640
+ return
641
+ globals()[global_name] = value
642
+ os.environ[env_key] = value
643
+
644
+ shared_endpoint = _clean(endpoint)
645
+ shared_api_version = _clean(api_version)
646
+ shared_api_key = _clean(api_key)
647
+ shared_auth_mode = _clean(auth_mode, lower=True)
648
+ shared_ad_scope = _clean(ad_scope)
649
+ shared_managed_identity_client_id = _clean(managed_identity_client_id)
650
+
651
+ # Auto-configure for openai_compatible mode
652
+ if shared_auth_mode in {"openai_compatible", "compat", "openai"}:
653
+ if shared_api_version is None:
654
+ shared_api_version = _OPENAI_COMPATIBLE_API_VERSION
655
+
656
+ _set("ENDPOINT", shared_endpoint, "AZURE_OPENAI_ENDPOINT")
657
+ _set("API_VERSION", shared_api_version, "AZURE_OPENAI_API_VERSION")
658
+ _set("API_KEY", shared_api_key, "AZURE_OPENAI_API_KEY")
659
+ _set("AUTH_MODE", shared_auth_mode, "AZURE_OPENAI_AUTH_MODE")
660
+ _set("AD_SCOPE", shared_ad_scope, "AZURE_OPENAI_AD_SCOPE")
661
+ _set(
662
+ "MANAGED_IDENTITY_CLIENT_ID",
663
+ shared_managed_identity_client_id,
664
+ "AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID",
665
+ )
666
+
667
+ resolved_optimizer_endpoint = _clean(optimizer_endpoint) or shared_endpoint
668
+ resolved_optimizer_api_version = _clean(optimizer_api_version) or shared_api_version
669
+ resolved_optimizer_api_key = _clean(optimizer_api_key) or shared_api_key
670
+ resolved_optimizer_auth_mode = _clean(optimizer_auth_mode, lower=True) or shared_auth_mode
671
+ resolved_optimizer_ad_scope = _clean(optimizer_ad_scope) or shared_ad_scope
672
+ resolved_optimizer_mi = (
673
+ _clean(optimizer_managed_identity_client_id)
674
+ or shared_managed_identity_client_id
675
+ )
676
+
677
+ # Auto-configure for openai_compatible mode
678
+ if resolved_optimizer_auth_mode in {"openai_compatible", "compat", "openai"}:
679
+ if resolved_optimizer_api_version is None:
680
+ resolved_optimizer_api_version = _OPENAI_COMPATIBLE_API_VERSION
681
+
682
+ resolved_target_endpoint = _clean(target_endpoint) or shared_endpoint
683
+ resolved_target_api_version = _clean(target_api_version) or shared_api_version
684
+ resolved_target_api_key = _clean(target_api_key) or shared_api_key
685
+ resolved_target_auth_mode = _clean(target_auth_mode, lower=True) or shared_auth_mode
686
+ resolved_target_ad_scope = _clean(target_ad_scope) or shared_ad_scope
687
+ resolved_target_mi = (
688
+ _clean(target_managed_identity_client_id)
689
+ or shared_managed_identity_client_id
690
+ )
691
+
692
+ # Auto-configure for openai_compatible mode
693
+ if resolved_target_auth_mode in {"openai_compatible", "compat", "openai"}:
694
+ if resolved_target_api_version is None:
695
+ resolved_target_api_version = _OPENAI_COMPATIBLE_API_VERSION
696
+
697
+ _set("OPTIMIZER_ENDPOINT", resolved_optimizer_endpoint, "OPTIMIZER_AZURE_OPENAI_ENDPOINT")
698
+ _set(
699
+ "OPTIMIZER_API_VERSION",
700
+ resolved_optimizer_api_version,
701
+ "OPTIMIZER_AZURE_OPENAI_API_VERSION",
702
+ )
703
+ _set("OPTIMIZER_API_KEY", resolved_optimizer_api_key, "OPTIMIZER_AZURE_OPENAI_API_KEY")
704
+ _set("OPTIMIZER_AUTH_MODE", resolved_optimizer_auth_mode, "OPTIMIZER_AZURE_OPENAI_AUTH_MODE")
705
+ _set("OPTIMIZER_AD_SCOPE", resolved_optimizer_ad_scope, "OPTIMIZER_AZURE_OPENAI_AD_SCOPE")
706
+ _set(
707
+ "OPTIMIZER_MANAGED_IDENTITY_CLIENT_ID",
708
+ resolved_optimizer_mi,
709
+ "OPTIMIZER_AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID",
710
+ )
711
+ _set("TARGET_ENDPOINT", resolved_target_endpoint, "TARGET_AZURE_OPENAI_ENDPOINT")
712
+ _set(
713
+ "TARGET_API_VERSION",
714
+ resolved_target_api_version,
715
+ "TARGET_AZURE_OPENAI_API_VERSION",
716
+ )
717
+ _set("TARGET_API_KEY", resolved_target_api_key, "TARGET_AZURE_OPENAI_API_KEY")
718
+ _set("TARGET_AUTH_MODE", resolved_target_auth_mode, "TARGET_AZURE_OPENAI_AUTH_MODE")
719
+ _set("TARGET_AD_SCOPE", resolved_target_ad_scope, "TARGET_AZURE_OPENAI_AD_SCOPE")
720
+ _set(
721
+ "TARGET_MANAGED_IDENTITY_CLIENT_ID",
722
+ resolved_target_mi,
723
+ "TARGET_AZURE_OPENAI_MANAGED_IDENTITY_CLIENT_ID",
724
+ )
725
+
726
+ with _optimizer_lock:
727
+ _optimizer_client = None
728
+ with _target_lock:
729
+ _target_client = None
730
+
731
+
732
+ def chat_optimizer(
733
+ system: str,
734
+ user: str,
735
+ max_completion_tokens: int = 16384,
736
+ retries: int = 5,
737
+ stage: str = "optimizer",
738
+ reasoning_effort: str | None = None,
739
+ timeout: int | None = None,
740
+ ) -> tuple[str, dict]:
741
+ """Call the optimizer model. Returns (response_text, usage_dict)."""
742
+ return _chat_impl(
743
+ get_optimizer_client(), OPTIMIZER_DEPLOYMENT,
744
+ system, user, max_completion_tokens, retries, stage, reasoning_effort, timeout,
745
+ )
746
+
747
+
748
+ def chat_with_deployment(
749
+ deployment: str,
750
+ system: str,
751
+ user: str,
752
+ max_completion_tokens: int = 16384,
753
+ retries: int = 5,
754
+ stage: str = "custom",
755
+ reasoning_effort: str | None = None,
756
+ timeout: int | None = None,
757
+ ) -> tuple[str, dict]:
758
+ """Call an arbitrary deployment using the shared Azure client."""
759
+ return _chat_impl(
760
+ get_optimizer_client(),
761
+ deployment,
762
+ system,
763
+ user,
764
+ max_completion_tokens,
765
+ retries,
766
+ stage,
767
+ reasoning_effort,
768
+ timeout,
769
+ )
770
+
771
+
772
+ def chat_target(
773
+ system: str,
774
+ user: str,
775
+ max_completion_tokens: int = 16384,
776
+ retries: int = 5,
777
+ stage: str = "target",
778
+ reasoning_effort: str | None = None,
779
+ timeout: int | None = None,
780
+ ) -> tuple[str, dict]:
781
+ """Call the target model. Returns (response_text, usage_dict)."""
782
+ return _chat_impl(
783
+ get_target_client(), TARGET_DEPLOYMENT,
784
+ system, user, max_completion_tokens, retries, stage, reasoning_effort, timeout,
785
+ )
786
+
787
+
788
+ def chat_optimizer_messages(
789
+ messages: list[dict[str, Any]],
790
+ max_completion_tokens: int = 16384,
791
+ retries: int = 5,
792
+ stage: str = "optimizer",
793
+ reasoning_effort: str | None = None,
794
+ *,
795
+ tools: list[dict[str, Any]] | None = None,
796
+ tool_choice: str | dict[str, Any] | None = None,
797
+ return_message: bool = False,
798
+ timeout: int | None = None,
799
+ ) -> tuple[Any, dict]:
800
+ """Call the optimizer model with a pre-built chat message list."""
801
+ return _chat_messages_impl(
802
+ get_optimizer_client(),
803
+ OPTIMIZER_DEPLOYMENT,
804
+ messages,
805
+ max_completion_tokens,
806
+ retries,
807
+ stage,
808
+ reasoning_effort,
809
+ tools=tools,
810
+ tool_choice=tool_choice,
811
+ return_message=return_message,
812
+ timeout=timeout,
813
+ )
814
+
815
+
816
+ def chat_messages_with_deployment(
817
+ deployment: str,
818
+ messages: list[dict[str, Any]],
819
+ max_completion_tokens: int = 16384,
820
+ retries: int = 5,
821
+ stage: str = "custom",
822
+ reasoning_effort: str | None = None,
823
+ *,
824
+ tools: list[dict[str, Any]] | None = None,
825
+ tool_choice: str | dict[str, Any] | None = None,
826
+ return_message: bool = False,
827
+ timeout: int | None = None,
828
+ ) -> tuple[Any, dict]:
829
+ """Call an arbitrary deployment with a pre-built chat message list."""
830
+ return _chat_messages_impl(
831
+ get_optimizer_client(),
832
+ deployment,
833
+ messages,
834
+ max_completion_tokens,
835
+ retries,
836
+ stage,
837
+ reasoning_effort,
838
+ tools=tools,
839
+ tool_choice=tool_choice,
840
+ return_message=return_message,
841
+ timeout=timeout,
842
+ )
843
+
844
+
845
+ def chat_target_messages(
846
+ messages: list[dict[str, Any]],
847
+ max_completion_tokens: int = 16384,
848
+ retries: int = 5,
849
+ stage: str = "target",
850
+ reasoning_effort: str | None = None,
851
+ *,
852
+ tools: list[dict[str, Any]] | None = None,
853
+ tool_choice: str | dict[str, Any] | None = None,
854
+ return_message: bool = False,
855
+ timeout: int | None = None,
856
+ ) -> tuple[Any, dict]:
857
+ """Call the target model with a pre-built chat message list."""
858
+ return _chat_messages_impl(
859
+ get_target_client(),
860
+ TARGET_DEPLOYMENT,
861
+ messages,
862
+ max_completion_tokens,
863
+ retries,
864
+ stage,
865
+ reasoning_effort,
866
+ tools=tools,
867
+ tool_choice=tool_choice,
868
+ return_message=return_message,
869
+ timeout=timeout,
870
+ )
871
+
872
+
873
+ def get_token_summary() -> dict:
874
+ """Return per-stage and total token usage."""
875
+ return tracker.summary()
876
+
877
+
878
+ def reset_token_tracker() -> None:
879
+ tracker.reset()
880
+
881
+
882
+ def set_target_deployment(deployment: str) -> None:
883
+ """Change target deployment at runtime."""
884
+ global _target_client, TARGET_DEPLOYMENT
885
+ TARGET_DEPLOYMENT = deployment
886
+ os.environ["TARGET_DEPLOYMENT"] = deployment
887
+ os.environ["AZURE_OPENAI_DEPLOYMENT"] = deployment
888
+ with _target_lock:
889
+ _target_client = None
890
+ try:
891
+ import llm_client as _legacy
892
+ _legacy.DEPLOYMENT = deployment
893
+ _legacy._client = None
894
+ except Exception:
895
+ pass
896
+
897
+
898
+ def set_reasoning_effort(effort: str | None) -> None:
899
+ """Set reasoning effort for all LLM calls. None = off."""
900
+ global REASONING_EFFORT
901
+ REASONING_EFFORT = effort if effort else None
902
+
903
+
904
+ def get_reasoning_effort() -> str | None:
905
+ """Return the process-wide reasoning effort for direct Azure client users."""
906
+ return REASONING_EFFORT
907
+
908
+
909
+ def set_optimizer_deployment(deployment: str) -> None:
910
+ """Change optimizer deployment at runtime."""
911
+ global _optimizer_client, OPTIMIZER_DEPLOYMENT
912
+ OPTIMIZER_DEPLOYMENT = deployment
913
+ os.environ["OPTIMIZER_DEPLOYMENT"] = deployment
914
+ with _optimizer_lock:
915
+ _optimizer_client = None