A5-R3-R4C dormant ModelPolicy v2 execution seam

#113
core/generation_runner.py CHANGED
@@ -2888,11 +2888,53 @@ class GenerationRunner:
2888
  routes = model.get("routes") if isinstance(model.get("routes"), dict) else {}
2889
  if not routes:
2890
  raise RuntimeError("trusted_task_policy_model_routes_missing")
2891
- # A5-R3-R3A closes snapshot identity only. It deliberately does not
2892
- # reinterpret fine-grained logical-node routes as the legacy coarse
2893
- # capability roles. Product execution remains fail-closed until the
2894
- # separate logical-node execution contract is implemented.
2895
- raise RuntimeError("trusted_task_policy_model_route_contract_not_ready")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2896
 
2897
  def _run_impl(self) -> dict[str, Any]:
2898
  req = self.request
@@ -2921,6 +2963,8 @@ class GenerationRunner:
2921
  agent = SSciPipelineAgent(ds_key, s2_key)
2922
  self.agent = agent
2923
  agent.set_model_policy_snapshot(plan_policy_snapshot)
 
 
2924
  agent.set_cancel_checker(self.cancel_checker)
2925
  # DeepSeek/search activity marks the executor as genuinely alive; the
2926
  # heartbeat thread decides separately when to renew D1's lease.
@@ -3661,6 +3705,8 @@ class GenerationRunner:
3661
  # glossary order deterministic and avoids sharing mutable writer state.
3662
  translation_agent = SSciPipelineAgent(ds_key, s2_key)
3663
  translation_agent.set_model_policy_snapshot(plan_policy_snapshot)
 
 
3664
  self.translation_agent = translation_agent
3665
  translation_agent.set_execution_progress_hook(lambda: self._progress_activity())
3666
  translation_agent.set_cancel_checker(lambda: self.cancel_checker())
@@ -3902,6 +3948,9 @@ class GenerationRunner:
3902
  for word in ["abstract", "keyword", "conclusion", "references", "appendix"]
3903
  )
3904
  section_id = str(sec.get("section_id") or "")
 
 
 
3905
  planned_items = [
3906
  item for item in list(citation_plan.get(display_name, []))
3907
  if agent.citation_source_role_eligible(
@@ -4224,6 +4273,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
4224
  agent.stream_deepseek(
4225
  sys_prompt, candidate_user_prompt, max_tokens=7000,
4226
  model_role="writing", thinking=False, allow_fallback=False,
 
4227
  ),
4228
  preview_prefix=full_draft_en + prefix,
4229
  preview_stage=(display_name if section_attempt == 1 else f"{display_name}:retry"),
 
2888
  routes = model.get("routes") if isinstance(model.get("routes"), dict) else {}
2889
  if not routes:
2890
  raise RuntimeError("trusted_task_policy_model_routes_missing")
2891
+ if str(snapshot.get("model_policy_version") or "") not in {
2892
+ "model-standard-v2", "model-flagship-v2"
2893
+ }:
2894
+ # Preserve the dormant R3A v1 fail-closed contract; only v2 may
2895
+ # cross the new trusted logical-node execution seam.
2896
+ raise RuntimeError("trusted_task_policy_model_route_contract_not_ready")
2897
+ if str(model.get("provider_catalog_version") or "") != "deepseek-v4-concrete-v1":
2898
+ raise RuntimeError("trusted_task_policy_provider_catalog_invalid")
2899
+ return snapshot
2900
+
2901
+ @staticmethod
2902
+ def _section_logical_node(mode: str, section_id: str, subsection_id: str | None = None) -> str | None:
2903
+ """Map academic topology to a tier-neutral writer logical node."""
2904
+ mode = str(mode or "").strip().lower()
2905
+ section_id = str(section_id or "").strip().lower()
2906
+ subsection_id = str(subsection_id or "").strip().lower()
2907
+ if mode == "amj":
2908
+ if section_id in {"introduction", "theory"}:
2909
+ return "core_section_writing"
2910
+ if section_id == "methodology":
2911
+ return "methods_writing"
2912
+ if section_id == "results":
2913
+ return "quantitative_results_writing"
2914
+ if section_id == "discussion":
2915
+ if subsection_id in {"practical_implications", "limitations"}:
2916
+ return "ordinary_section_writing"
2917
+ return "core_section_writing"
2918
+ if section_id == "conclusion":
2919
+ return "ordinary_section_writing"
2920
+ elif mode == "asq":
2921
+ if section_id in {"introduction", "theoretical_framing"}:
2922
+ return "core_section_writing"
2923
+ if section_id == "methods":
2924
+ return "methods_writing"
2925
+ if section_id == "findings":
2926
+ if subsection_id == "emergent_model":
2927
+ return "core_section_writing"
2928
+ return "evidence_rich_findings_narration"
2929
+ if section_id == "discussion":
2930
+ if subsection_id in {"boundaries", "practical_implications"}:
2931
+ return "ordinary_section_writing"
2932
+ return "core_section_writing"
2933
+ if section_id == "conclusion":
2934
+ return "ordinary_section_writing"
2935
+ if section_id == "appendix":
2936
+ return None
2937
+ raise RuntimeError(f"logical_node_mapping_missing:{mode}:{section_id}:{subsection_id}")
2938
 
2939
  def _run_impl(self) -> dict[str, Any]:
2940
  req = self.request
 
2963
  agent = SSciPipelineAgent(ds_key, s2_key)
2964
  self.agent = agent
2965
  agent.set_model_policy_snapshot(plan_policy_snapshot)
2966
+ if trusted_task_policy_snapshot is not None:
2967
+ agent.set_trusted_product_policy_snapshot(trusted_task_policy_snapshot)
2968
  agent.set_cancel_checker(self.cancel_checker)
2969
  # DeepSeek/search activity marks the executor as genuinely alive; the
2970
  # heartbeat thread decides separately when to renew D1's lease.
 
3705
  # glossary order deterministic and avoids sharing mutable writer state.
3706
  translation_agent = SSciPipelineAgent(ds_key, s2_key)
3707
  translation_agent.set_model_policy_snapshot(plan_policy_snapshot)
3708
+ if trusted_task_policy_snapshot is not None:
3709
+ translation_agent.set_trusted_product_policy_snapshot(trusted_task_policy_snapshot)
3710
  self.translation_agent = translation_agent
3711
  translation_agent.set_execution_progress_hook(lambda: self._progress_activity())
3712
  translation_agent.set_cancel_checker(lambda: self.cancel_checker())
 
3948
  for word in ["abstract", "keyword", "conclusion", "references", "appendix"]
3949
  )
3950
  section_id = str(sec.get("section_id") or "")
3951
+ section_logical_node = self._section_logical_node(
3952
+ mode, section_id, sec.get("subsection_id")
3953
+ )
3954
  planned_items = [
3955
  item for item in list(citation_plan.get(display_name, []))
3956
  if agent.citation_source_role_eligible(
 
4273
  agent.stream_deepseek(
4274
  sys_prompt, candidate_user_prompt, max_tokens=7000,
4275
  model_role="writing", thinking=False, allow_fallback=False,
4276
+ logical_node=section_logical_node,
4277
  ),
4278
  preview_prefix=full_draft_en + prefix,
4279
  preview_stage=(display_name if section_attempt == 1 else f"{display_name}:retry"),
core/meta_extractor.py CHANGED
@@ -21,7 +21,8 @@ class MetaExtractor:
21
  prompt,
22
  snippet,
23
  max_tokens=1000,
24
- model_name="deepseek-chat"
 
25
  )
26
 
27
  # 强制提取 JSON 部分,过滤模型可能的解释性前缀或后缀
@@ -49,4 +50,4 @@ class MetaExtractor:
49
 
50
  def extract_golden_metadata(agent, paper_texts: List[str]):
51
  extractor = MetaExtractor(agent)
52
- return extractor.run_batch_extraction(paper_texts)
 
21
  prompt,
22
  snippet,
23
  max_tokens=1000,
24
+ model_name="deepseek-chat",
25
+ logical_node='literature_parsing',
26
  )
27
 
28
  # 强制提取 JSON 部分,过滤模型可能的解释性前缀或后缀
 
50
 
51
  def extract_golden_metadata(agent, paper_texts: List[str]):
52
  extractor = MetaExtractor(agent)
53
+ return extractor.run_batch_extraction(paper_texts)
core/model_gateway.py CHANGED
@@ -1,10 +1,11 @@
1
  # -*- coding: utf-8 -*-
2
  """Provider-neutral model routing primitives for SmartPaper.
3
 
4
- A2 keeps the currently verified DeepSeek transport intact while moving provider
5
- selection out of academic business logic. Providers are registered adapters;
6
- academic code asks for a capability (reasoning/writing/translation/fast/repair)
7
- and never needs to know the concrete vendor or model name.
 
8
  """
9
  from __future__ import annotations
10
 
@@ -40,14 +41,18 @@ class ModelRoute:
40
  fallback_providers: tuple[str, ...] = ()
41
 
42
 
43
- class ProviderNeutralModelGateway:
44
- """Route capability calls to registered provider adapters.
 
 
 
 
 
 
45
 
46
- Adapter callables intentionally share the existing PipelineAgent request
47
- signature. This makes A2 a low-risk architectural seam: DeepSeek remains
48
- the verified adapter today, while Gemini/Qwen/OpenAI adapters can be
49
- registered later without changing manuscript generation code.
50
- """
51
 
52
  def __init__(self, config: dict[str, Any] | None = None):
53
  cfg = dict(config or {})
@@ -61,6 +66,7 @@ class ProviderNeutralModelGateway:
61
  if isinstance(row, str):
62
  provider = str(row or self.default_provider).strip().lower()
63
  provider_role = cap
 
64
  else:
65
  item = dict(row or {})
66
  provider = str(item.get("provider") or self.default_provider).strip().lower()
@@ -73,12 +79,15 @@ class ProviderNeutralModelGateway:
73
  for name in raw_fallbacks
74
  if str(name or "").strip()
75
  )
76
- if not isinstance(row, dict):
77
- fallbacks = ()
78
- self._routes[cap] = ModelRoute(provider=provider, provider_role=provider_role, fallback_providers=fallbacks)
 
 
79
  self._call_adapters: dict[str, Callable[..., str]] = {}
80
  self._stream_adapters: dict[str, Callable[..., Iterable[str]]] = {}
81
  self._policy_snapshot: dict[str, Any] = {}
 
82
 
83
  def register_provider(
84
  self,
@@ -93,6 +102,7 @@ class ProviderNeutralModelGateway:
93
  self._call_adapters[name] = call_adapter
94
  self._stream_adapters[name] = stream_adapter
95
 
 
96
  def set_policy_snapshot(self, snapshot: dict[str, Any] | None) -> None:
97
  self._policy_snapshot = deepcopy(dict(snapshot or {}))
98
 
@@ -100,6 +110,19 @@ class ProviderNeutralModelGateway:
100
  def policy_snapshot(self) -> dict[str, Any]:
101
  return deepcopy(self._policy_snapshot)
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def _route(self, capability: str) -> ModelRoute:
104
  cap = str(capability or "reasoning").strip().lower() or "reasoning"
105
  policy_routes = dict(self._policy_snapshot.get("model_routes") or {})
@@ -114,7 +137,8 @@ class ProviderNeutralModelGateway:
114
  provider=str(row.get("provider") or self.default_provider).strip().lower(),
115
  provider_role=str(row.get("provider_role") or cap).strip().lower(),
116
  fallback_providers=tuple(
117
- str(name or "").strip().lower() for name in raw_fallbacks
 
118
  if str(name or "").strip()
119
  ),
120
  )
@@ -126,7 +150,118 @@ class ProviderNeutralModelGateway:
126
  name for name in (route.provider, *route.fallback_providers) if name
127
  ))
128
 
129
- def call(self, *, capability: str, **kwargs) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  route = self._route(capability)
131
  missing = []
132
  for provider in self._provider_sequence(route):
@@ -141,7 +276,19 @@ class ProviderNeutralModelGateway:
141
  f"model_provider_unavailable:{','.join(missing) or route.provider}:{capability}"
142
  )
143
 
144
- def stream(self, *, capability: str, **kwargs):
 
 
 
 
 
 
 
 
 
 
 
 
145
  route = self._route(capability)
146
  missing = []
147
  for provider in self._provider_sequence(route):
@@ -158,20 +305,17 @@ class ProviderNeutralModelGateway:
158
  )
159
 
160
 
 
 
 
 
 
161
  def resolve_plan_policy_snapshot(
162
  app_config: dict[str, Any], request: dict[str, Any] | None = None
163
  ) -> dict[str, Any]:
164
- """Freeze a config-driven plan policy without touching payment authority.
165
-
166
- A2 only creates the interface. Worker/D1 remain the authority for payment,
167
- quota and entitlement; if the request does not yet carry a trusted plan_id,
168
- the legacy-current policy preserves existing production behavior.
169
- """
170
  req = dict(request or {})
171
  policies = dict(app_config.get("plan_policies") or {})
172
- # Never trust a browser/request plan label as an entitlement authority.
173
- # A future Worker/D1 integration may inject a trusted entitlement snapshot;
174
- # until then every task stays on the legacy-current production policy.
175
  entitlement = req.get("entitlement_snapshot") if isinstance(req.get("entitlement_snapshot"), dict) else {}
176
  requested = str(
177
  req.get("trusted_plan_id") or entitlement.get("plan_id") or ""
 
1
  # -*- coding: utf-8 -*-
2
  """Provider-neutral model routing primitives for SmartPaper.
3
 
4
+ Legacy tasks continue to route by coarse capability. A5-R3-R4B adds a
5
+ separate, trusted product-policy seam: academic callers provide an explicit
6
+ logical_node, while the frozen TaskPolicySnapshot supplies provider/model and
7
+ execution settings. Product routing never falls back to legacy capability
8
+ routes.
9
  """
10
  from __future__ import annotations
11
 
 
41
  fallback_providers: tuple[str, ...] = ()
42
 
43
 
44
+ @dataclass(frozen=True)
45
+ class TrustedProductRoute:
46
+ provider: str
47
+ model_id: str
48
+ thinking_enabled: bool
49
+ reasoning_effort: str | None
50
+ max_output_tokens: int
51
+ retry_budget: int
52
 
53
+
54
+ class ProviderNeutralModelGateway:
55
+ """Route legacy capabilities or trusted product logical nodes to adapters."""
 
 
56
 
57
  def __init__(self, config: dict[str, Any] | None = None):
58
  cfg = dict(config or {})
 
66
  if isinstance(row, str):
67
  provider = str(row or self.default_provider).strip().lower()
68
  provider_role = cap
69
+ fallbacks = ()
70
  else:
71
  item = dict(row or {})
72
  provider = str(item.get("provider") or self.default_provider).strip().lower()
 
79
  for name in raw_fallbacks
80
  if str(name or "").strip()
81
  )
82
+ self._routes[cap] = ModelRoute(
83
+ provider=provider,
84
+ provider_role=provider_role,
85
+ fallback_providers=fallbacks,
86
+ )
87
  self._call_adapters: dict[str, Callable[..., str]] = {}
88
  self._stream_adapters: dict[str, Callable[..., Iterable[str]]] = {}
89
  self._policy_snapshot: dict[str, Any] = {}
90
+ self._trusted_product_policy_snapshot: dict[str, Any] = {}
91
 
92
  def register_provider(
93
  self,
 
102
  self._call_adapters[name] = call_adapter
103
  self._stream_adapters[name] = stream_adapter
104
 
105
+ # Legacy config/request policy. It remains a separate trust domain.
106
  def set_policy_snapshot(self, snapshot: dict[str, Any] | None) -> None:
107
  self._policy_snapshot = deepcopy(dict(snapshot or {}))
108
 
 
110
  def policy_snapshot(self) -> dict[str, Any]:
111
  return deepcopy(self._policy_snapshot)
112
 
113
+ def set_trusted_product_policy_snapshot(self, snapshot: dict[str, Any] | None) -> None:
114
+ """Attach a Worker/D1-verified TaskPolicySnapshot for product execution.
115
+
116
+ The runner validates task identity and snapshot SHA before calling this
117
+ setter. The gateway still validates route shape and fails closed before
118
+ invoking any provider adapter.
119
+ """
120
+ self._trusted_product_policy_snapshot = deepcopy(dict(snapshot or {}))
121
+
122
+ @property
123
+ def trusted_product_policy_snapshot(self) -> dict[str, Any]:
124
+ return deepcopy(self._trusted_product_policy_snapshot)
125
+
126
  def _route(self, capability: str) -> ModelRoute:
127
  cap = str(capability or "reasoning").strip().lower() or "reasoning"
128
  policy_routes = dict(self._policy_snapshot.get("model_routes") or {})
 
137
  provider=str(row.get("provider") or self.default_provider).strip().lower(),
138
  provider_role=str(row.get("provider_role") or cap).strip().lower(),
139
  fallback_providers=tuple(
140
+ str(name or "").strip().lower()
141
+ for name in raw_fallbacks
142
  if str(name or "").strip()
143
  ),
144
  )
 
150
  name for name in (route.provider, *route.fallback_providers) if name
151
  ))
152
 
153
+ def _trusted_product_context(self) -> tuple[dict[str, Any], dict[str, Any]] | None:
154
+ snapshot = self._trusted_product_policy_snapshot
155
+ if not snapshot:
156
+ return None
157
+ resolved = snapshot.get("resolved_policy")
158
+ if not isinstance(resolved, dict):
159
+ raise ModelRequestError("trusted_product_policy_invalid")
160
+ model = resolved.get("model")
161
+ if not isinstance(model, dict):
162
+ raise ModelRequestError("trusted_product_model_policy_missing")
163
+ routes = model.get("routes")
164
+ if not isinstance(routes, dict) or not routes:
165
+ raise ModelRequestError("trusted_product_model_routes_missing")
166
+ return resolved, routes
167
+
168
+ def _trusted_product_route(self, logical_node: str | None) -> tuple[TrustedProductRoute, str]:
169
+ context = self._trusted_product_context()
170
+ if context is None:
171
+ raise ModelRequestError("trusted_product_policy_missing")
172
+ resolved, routes = context
173
+ node = str(logical_node or "").strip()
174
+ if not node:
175
+ raise ModelRequestError("trusted_product_logical_node_required")
176
+ row = routes.get(node)
177
+ if not isinstance(row, dict):
178
+ raise ModelRequestError(f"trusted_product_route_missing:{node}")
179
+
180
+ provider = str(row.get("provider") or "").strip().lower()
181
+ model_id = str(row.get("model_id_or_alias") or "").strip()
182
+ if not provider or not model_id:
183
+ raise ModelRequestError(f"trusted_product_route_invalid:{node}")
184
+
185
+ thinking = row.get("thinking_enabled")
186
+ if not isinstance(thinking, bool):
187
+ raise ModelRequestError(f"trusted_product_route_thinking_invalid:{node}")
188
+ effort = row.get("reasoning_effort")
189
+ if thinking:
190
+ if str(effort or "").strip().lower() != "high":
191
+ raise ModelRequestError(f"trusted_product_route_effort_invalid:{node}")
192
+ effort = "high"
193
+ else:
194
+ if effort not in (None, ""):
195
+ raise ModelRequestError(f"trusted_product_route_effort_invalid:{node}")
196
+ effort = None
197
+
198
+ try:
199
+ max_output_tokens = int(row.get("max_output_tokens"))
200
+ retry_budget = int(row.get("retry_budget"))
201
+ except (TypeError, ValueError):
202
+ raise ModelRequestError(f"trusted_product_route_budget_invalid:{node}")
203
+ if max_output_tokens <= 0 or retry_budget < 0:
204
+ raise ModelRequestError(f"trusted_product_route_budget_invalid:{node}")
205
+ if row.get("escalation_model_id_or_alias") not in (None, ""):
206
+ raise ModelRequestError(f"trusted_product_route_escalation_forbidden:{node}")
207
+ if row.get("escalation_condition_code") not in (None, ""):
208
+ raise ModelRequestError(f"trusted_product_route_escalation_forbidden:{node}")
209
+
210
+ model_policy_version = str(
211
+ resolved.get("model_policy_version")
212
+ or snapshot_model_policy_version(self._trusted_product_policy_snapshot)
213
+ or ""
214
+ ).strip()
215
+ return TrustedProductRoute(
216
+ provider=provider,
217
+ model_id=model_id,
218
+ thinking_enabled=thinking,
219
+ reasoning_effort=effort,
220
+ max_output_tokens=max_output_tokens,
221
+ retry_budget=retry_budget,
222
+ ), model_policy_version
223
+
224
+ @staticmethod
225
+ def _effective_max_tokens(requested: Any, ceiling: int) -> int:
226
+ if requested is None:
227
+ return int(ceiling)
228
+ try:
229
+ value = int(requested)
230
+ except (TypeError, ValueError):
231
+ raise ModelRequestError("trusted_product_max_tokens_invalid")
232
+ if value <= 0:
233
+ raise ModelRequestError("trusted_product_max_tokens_invalid")
234
+ return min(value, int(ceiling))
235
+
236
+ def _product_payload(self, *, capability: str, logical_node: str | None, kwargs: dict[str, Any]):
237
+ route, model_policy_version = self._trusted_product_route(logical_node)
238
+ payload = dict(kwargs)
239
+ payload["model_role"] = str(capability or "reasoning").strip().lower() or "reasoning"
240
+ payload["model_name"] = route.model_id
241
+ payload["thinking"] = route.thinking_enabled
242
+ payload["reasoning_effort"] = route.reasoning_effort
243
+ payload["allow_fallback"] = False
244
+ payload["max_tokens"] = self._effective_max_tokens(
245
+ payload.get("max_tokens"), route.max_output_tokens
246
+ )
247
+ payload["retry_budget"] = route.retry_budget
248
+ payload["logical_node"] = str(logical_node or "").strip()
249
+ payload["model_policy_version"] = model_policy_version
250
+ payload["route_enforced"] = True
251
+ return route.provider, payload
252
+
253
+ def call(self, *, capability: str, logical_node: str | None = None, **kwargs) -> str:
254
+ if self._trusted_product_context() is not None:
255
+ provider, payload = self._product_payload(
256
+ capability=capability, logical_node=logical_node, kwargs=kwargs
257
+ )
258
+ adapter = self._call_adapters.get(provider)
259
+ if not callable(adapter):
260
+ raise ModelProviderUnavailableError(
261
+ f"model_provider_unavailable:{provider}:{logical_node or capability}"
262
+ )
263
+ return adapter(**payload)
264
+
265
  route = self._route(capability)
266
  missing = []
267
  for provider in self._provider_sequence(route):
 
276
  f"model_provider_unavailable:{','.join(missing) or route.provider}:{capability}"
277
  )
278
 
279
+ def stream(self, *, capability: str, logical_node: str | None = None, **kwargs):
280
+ if self._trusted_product_context() is not None:
281
+ provider, payload = self._product_payload(
282
+ capability=capability, logical_node=logical_node, kwargs=kwargs
283
+ )
284
+ adapter = self._stream_adapters.get(provider)
285
+ if not callable(adapter):
286
+ raise ModelProviderUnavailableError(
287
+ f"model_provider_unavailable:{provider}:{logical_node or capability}"
288
+ )
289
+ yield from adapter(**payload)
290
+ return
291
+
292
  route = self._route(capability)
293
  missing = []
294
  for provider in self._provider_sequence(route):
 
305
  )
306
 
307
 
308
+ def snapshot_model_policy_version(snapshot: dict[str, Any] | None) -> str:
309
+ data = dict(snapshot or {})
310
+ return str(data.get("model_policy_version") or "").strip()
311
+
312
+
313
  def resolve_plan_policy_snapshot(
314
  app_config: dict[str, Any], request: dict[str, Any] | None = None
315
  ) -> dict[str, Any]:
316
+ """Freeze a config-driven legacy plan policy without touching payment authority."""
 
 
 
 
 
317
  req = dict(request or {})
318
  policies = dict(app_config.get("plan_policies") or {})
 
 
 
319
  entitlement = req.get("entitlement_snapshot") if isinstance(req.get("entitlement_snapshot"), dict) else {}
320
  requested = str(
321
  req.get("trusted_plan_id") or entitlement.get("plan_id") or ""
core/pipeline_agent.py CHANGED
@@ -272,7 +272,7 @@ class SSciPipelineAgent:
272
  self, *, usage, model, role, attempt_index, streaming, thinking,
273
  reasoning_effort, system_prompt, user_prompt, started_at,
274
  completed_at, first_token_at=None, finish_reason="",
275
- response_payload=None,
276
  ):
277
  if not self._model_usage_enabled():
278
  return
@@ -308,6 +308,9 @@ class SSciPipelineAgent:
308
  metadata=sanitize_metadata({
309
  "system_fingerprint": response_payload.get("system_fingerprint"),
310
  "reasoning_effort": reasoning_effort,
 
 
 
311
  }),
312
  )
313
  emit_usage_event(getattr(self, "_model_usage_sink", None), event)
@@ -851,8 +854,13 @@ class SSciPipelineAgent:
851
  requested_model,
852
  requested_thinking,
853
  allow_fallback,
 
854
  ):
855
- total = self.deepseek_max_retries
 
 
 
 
856
  fallback_model = self.deepseek_models.get("fallback", "")
857
  for attempt in range(total):
858
  model = requested_model
@@ -877,7 +885,8 @@ class SSciPipelineAgent:
877
  thinking=None,
878
  reasoning_effort=None,
879
  allow_fallback=None,
880
- response_format=None,
 
881
  ):
882
  if not str(getattr(self, "ds_key", "") or "").strip():
883
  raise DeepSeekAuthenticationError(
@@ -896,6 +905,15 @@ class SSciPipelineAgent:
896
  reasoning_effort=reasoning_effort,
897
  allow_fallback=allow_fallback,
898
  )
 
 
 
 
 
 
 
 
 
899
  tokens_est = self._count_tokens(str(user_prompt))
900
 
901
  logging.info(
@@ -913,6 +931,7 @@ class SSciPipelineAgent:
913
  requested_model,
914
  requested_thinking,
915
  resolved_fallback,
 
916
  ):
917
  with self.lock:
918
  self.ds_calls += 1
@@ -942,7 +961,7 @@ class SSciPipelineAgent:
942
  logging.info(
943
  "DeepSeek static attempt start | attempt=%s/%s | model=%s",
944
  attempt + 1,
945
- self.deepseek_max_retries,
946
  current_model,
947
  )
948
  response = requests.post(
@@ -1000,6 +1019,8 @@ class SSciPipelineAgent:
1000
  completed_at=completed_at,
1001
  finish_reason=choices[0].get("finish_reason"),
1002
  response_payload=result,
 
 
1003
  )
1004
  return content
1005
 
@@ -1016,11 +1037,11 @@ class SSciPipelineAgent:
1016
  DeepSeekAPIError,
1017
  ) as exc:
1018
  last_error = exc
1019
- is_last = attempt == self.deepseek_max_retries - 1
1020
  logging.error(
1021
  "DeepSeek attempt %s/%s failed: %s",
1022
  attempt + 1,
1023
- self.deepseek_max_retries,
1024
  exc,
1025
  )
1026
  self._touch_execution_progress("deepseek_static_retry")
@@ -1030,7 +1051,7 @@ class SSciPipelineAgent:
1030
 
1031
  raise DeepSeekAPIError(
1032
  "DeepSeek API failed after "
1033
- f"{self.deepseek_max_retries} attempts: {last_error}"
1034
  )
1035
 
1036
  def _stream_deepseek_transport(
@@ -1044,7 +1065,8 @@ class SSciPipelineAgent:
1044
  reasoning_effort=None,
1045
  temp=0.3,
1046
  allow_fallback=None,
1047
- reasoning_only_recovery=False,
 
1048
  ):
1049
  if not str(getattr(self, "ds_key", "") or "").strip():
1050
  raise DeepSeekAuthenticationError(
@@ -1063,6 +1085,15 @@ class SSciPipelineAgent:
1063
  reasoning_effort=reasoning_effort,
1064
  allow_fallback=allow_fallback,
1065
  )
 
 
 
 
 
 
 
 
 
1066
  tokens_est = self._count_tokens(str(user_prompt))
1067
 
1068
  logging.info(
@@ -1080,6 +1111,7 @@ class SSciPipelineAgent:
1080
  requested_model,
1081
  requested_thinking,
1082
  resolved_fallback,
 
1083
  ):
1084
  with self.lock:
1085
  self.ds_calls += 1
@@ -1121,7 +1153,7 @@ class SSciPipelineAgent:
1121
  logging.info(
1122
  "DeepSeek stream attempt start | attempt=%s/%s | model=%s | idle_limit=%ss",
1123
  attempt + 1,
1124
- self.deepseek_max_retries,
1125
  current_model,
1126
  self.deepseek_stream_idle_seconds,
1127
  )
@@ -1260,6 +1292,8 @@ class SSciPipelineAgent:
1260
  first_token_at=first_content_at,
1261
  finish_reason=stream_finish_reason,
1262
  response_payload=stream_response_payload,
 
 
1263
  )
1264
  return
1265
 
@@ -1285,7 +1319,7 @@ class SSciPipelineAgent:
1285
  logging.error(
1286
  "DeepSeek stream attempt %s/%s failed: %s",
1287
  attempt + 1,
1288
- self.deepseek_max_retries,
1289
  exc,
1290
  )
1291
 
@@ -1296,24 +1330,28 @@ class SSciPipelineAgent:
1296
  ) from exc
1297
 
1298
  self._touch_execution_progress("deepseek_stream_retry")
1299
- is_last = attempt == self.deepseek_max_retries - 1
1300
  if is_last:
1301
  break
1302
  time.sleep(self._retry_delay(attempt))
1303
 
1304
  raise DeepSeekAPIError(
1305
  "DeepSeek streaming failed after "
1306
- f"{self.deepseek_max_retries} attempts: {last_error}"
1307
  )
1308
 
1309
  def set_model_policy_snapshot(self, snapshot):
1310
  self.model_policy_snapshot = dict(snapshot or {})
1311
  self.model_gateway.set_policy_snapshot(self.model_policy_snapshot)
1312
 
 
 
 
1313
  def call_model(
1314
  self, system_prompt, user_prompt, max_tokens=8000, temp=0.3,
1315
  model_name=None, model_role="reasoning", thinking=None,
1316
  reasoning_effort=None, allow_fallback=None, response_format=None,
 
1317
  ):
1318
  gateway = getattr(self, "model_gateway", None)
1319
  if gateway is None:
@@ -1326,7 +1364,8 @@ class SSciPipelineAgent:
1326
  response_format=response_format,
1327
  )
1328
  return gateway.call(
1329
- capability=model_role, system_prompt=system_prompt, user_prompt=user_prompt,
 
1330
  max_tokens=max_tokens, temp=temp, model_name=model_name,
1331
  thinking=thinking, reasoning_effort=reasoning_effort,
1332
  allow_fallback=allow_fallback, response_format=response_format,
@@ -1335,7 +1374,7 @@ class SSciPipelineAgent:
1335
  def stream_model(
1336
  self, system_prompt, user_prompt, max_tokens=8000, model_name=None,
1337
  model_role="reasoning", thinking=None, reasoning_effort=None, temp=0.3,
1338
- allow_fallback=None, reasoning_only_recovery=False,
1339
  ):
1340
  gateway = getattr(self, "model_gateway", None)
1341
  if gateway is None:
@@ -1347,7 +1386,8 @@ class SSciPipelineAgent:
1347
  )
1348
  return
1349
  yield from gateway.stream(
1350
- capability=model_role, system_prompt=system_prompt, user_prompt=user_prompt,
 
1351
  max_tokens=max_tokens, model_name=model_name, thinking=thinking,
1352
  reasoning_effort=reasoning_effort, temp=temp, allow_fallback=allow_fallback,
1353
  reasoning_only_recovery=reasoning_only_recovery,
@@ -1359,24 +1399,25 @@ class SSciPipelineAgent:
1359
  self, system_prompt, user_prompt, max_tokens=8000, temp=0.3,
1360
  model_name=None, model_role="reasoning", thinking=None,
1361
  reasoning_effort=None, allow_fallback=None, response_format=None,
 
1362
  ):
1363
  return self.call_model(
1364
  system_prompt, user_prompt, max_tokens=max_tokens, temp=temp,
1365
  model_name=model_name, model_role=model_role, thinking=thinking,
1366
  reasoning_effort=reasoning_effort, allow_fallback=allow_fallback,
1367
- response_format=response_format,
1368
  )
1369
 
1370
  def stream_deepseek(
1371
  self, system_prompt, user_prompt, max_tokens=8000, model_name=None,
1372
  model_role="reasoning", thinking=None, reasoning_effort=None, temp=0.3,
1373
- allow_fallback=None, reasoning_only_recovery=False,
1374
  ):
1375
  yield from self.stream_model(
1376
  system_prompt, user_prompt, max_tokens=max_tokens, model_name=model_name,
1377
  model_role=model_role, thinking=thinking, reasoning_effort=reasoning_effort,
1378
  temp=temp, allow_fallback=allow_fallback,
1379
- reasoning_only_recovery=reasoning_only_recovery,
1380
  )
1381
 
1382
  def parse_user_core_literature(self, user_lit_text):
@@ -1398,6 +1439,7 @@ class SSciPipelineAgent:
1398
  model_role="reasoning",
1399
  thinking=True,
1400
  allow_fallback=False,
 
1401
  )
1402
  papers = self._extract_json_from_text(response)
1403
  if not papers:
@@ -1461,7 +1503,7 @@ class SSciPipelineAgent:
1461
  user_prompt = APP_CONFIG["prompt_templates"]["synthesis_user"].replace(
1462
  "{topic}", topic).replace("{user_injection}", user_injection).replace(
1463
  "{refs_text}", refs_text).replace("{directive}", APP_CONFIG['deep_synthesis_directive'])
1464
- return self.call_deepseek(sys_prompt, user_prompt, temp=0.2)
1465
 
1466
  def generate_anchors(
1467
  self,
@@ -1504,6 +1546,7 @@ class SSciPipelineAgent:
1504
  model_role="reasoning",
1505
  thinking=False,
1506
  allow_fallback=False,
 
1507
  )
1508
 
1509
  return _stream_direct_synthesis()
@@ -1562,6 +1605,7 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
1562
  model_role="reasoning",
1563
  thinking=False,
1564
  allow_fallback=False,
 
1565
  )
1566
 
1567
  return _stream_direct_research_logic()
@@ -1668,6 +1712,7 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
1668
  thinking=False,
1669
  allow_fallback=False,
1670
  response_format={"type": "json_object"},
 
1671
  )
1672
  try:
1673
  parsed = self._extract_json_from_text(response)
@@ -2630,6 +2675,7 @@ Return ONLY a valid JSON array of strings."""
2630
  user_prompt,
2631
  temp=0.1 + attempt * 0.1,
2632
  model_role="fast",
 
2633
  ))
2634
  if queries and isinstance(queries, list) and len(queries) > 0:
2635
  final_queries = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())]
@@ -3082,6 +3128,7 @@ Return ONLY a valid JSON array of strings."""
3082
  f"Topic: {topic}\nKeywords: {keywords}\nUsed: {sorted(all_used_terms)}",
3083
  temp=0.4,
3084
  model_role='fast',
 
3085
  )
3086
  parsed = self._extract_json_from_text(response)
3087
  terms = parsed if isinstance(parsed, list) else []
@@ -3287,6 +3334,7 @@ INPUT:
3287
  model_role='reasoning',
3288
  thinking=False,
3289
  allow_fallback=False,
 
3290
  )
3291
  parsed = self._extract_json_from_text(raw)
3292
  parsed_rows = parsed.get('assignments') if isinstance(parsed, dict) else None
@@ -5243,6 +5291,7 @@ INPUT:
5243
  model_role='reasoning',
5244
  thinking=False,
5245
  allow_fallback=False,
 
5246
  )
5247
  parsed = self._extract_json_from_text(response)
5248
  if isinstance(parsed, dict):
@@ -5386,6 +5435,7 @@ Return exactly:
5386
  model_role="reasoning",
5387
  thinking=False,
5388
  allow_fallback=False,
 
5389
  )
5390
  parsed = self._extract_json_from_text(response)
5391
  last_error = None
@@ -5439,6 +5489,7 @@ Return exactly:
5439
  model_role="reasoning",
5440
  thinking=False,
5441
  allow_fallback=False,
 
5442
  )
5443
  parsed = self._extract_json_from_text(response)
5444
  by_id = index_items(parsed)
@@ -5911,6 +5962,7 @@ TEXT:
5911
  model_role='writing',
5912
  thinking=False,
5913
  allow_fallback=False,
 
5914
  )
5915
  revised = self._remove_ai_meta_traces(revised)
5916
  original_numbers = self._numeric_tokens(value)
@@ -5963,6 +6015,7 @@ TEXT:
5963
  raw = self.call_deepseek(
5964
  'You are an independent senior management-journal reviewer. Be exact and conservative.',
5965
  prompt, max_tokens=800, temp=0.0, model_role='fast', thinking=False, allow_fallback=True,
 
5966
  )
5967
  except PipelineCancelledError:
5968
  raise
@@ -6037,6 +6090,7 @@ CURRENT UNIT:
6037
  repaired = self.call_model(
6038
  'You are SmartPaper Adaptive Academic Repair. Preserve evidence truth and repair prose with Conciseness, Accuracy, and Rigor.',
6039
  prompt, max_tokens=7000, temp=0.15, model_role='repair', thinking=True, allow_fallback=True,
 
6040
  )
6041
  except PipelineCancelledError:
6042
  raise
@@ -8059,6 +8113,7 @@ CURRENT UNIT:
8059
  json.dumps({'ir_version': self.CANONICAL_GLOSSARY_IR_VERSION, 'terms': payload}, ensure_ascii=False),
8060
  max_tokens=max(1200, min(5000, 120 + len(payload) * 90)),
8061
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
8062
  )
8063
  parsed = self._extract_json_from_text(response)
8064
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
@@ -8321,6 +8376,7 @@ CURRENT UNIT:
8321
  json.dumps({'ir_version': self.CANONICAL_GLOSSARY_IR_VERSION, 'terms': payload}, ensure_ascii=False),
8322
  max_tokens=max(1200, min(4200, 180 + len(payload) * 120)),
8323
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
8324
  )
8325
  parsed = self._extract_json_from_text(response)
8326
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
@@ -8453,6 +8509,7 @@ CURRENT UNIT:
8453
  json.dumps({'terms': missing}, ensure_ascii=False),
8454
  max_tokens=max(1200, min(5000, len(missing) * 80)),
8455
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
8456
  )
8457
  parsed = self._extract_json_from_text(response)
8458
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
@@ -8600,6 +8657,7 @@ SECTION: {section_name}
8600
  response = self.call_deepseek(
8601
  'You are a bilingual academic translation equivalence auditor. Be conservative and evidence-preserving.',
8602
  prompt, max_tokens=1200, temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
 
8603
  )
8604
  parsed = self._extract_json_from_text(response)
8605
  ok = parsed.get('ok') if isinstance(parsed, dict) else None
@@ -8691,6 +8749,7 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. For NEGATI
8691
  system_prompt, json.dumps(payload, ensure_ascii=False),
8692
  max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
8693
  temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
 
8694
  )
8695
  parsed = self._extract_json_from_text(response)
8696
  returned = parsed.get('items', []) if isinstance(parsed, dict) else []
@@ -8962,6 +9021,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
8962
  model_role='translation',
8963
  thinking=False,
8964
  allow_fallback=False,
 
8965
  )
8966
  parsed = self._extract_json_from_text(response)
8967
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
@@ -9151,6 +9211,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
9151
  effective_prompt, marked_source,
9152
  max_tokens=max(1800, min(9000, self._count_tokens(marked_source) * 2 + 1200)),
9153
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
9154
  )
9155
  try:
9156
  parsed = self._parse_marked_translation(result, expected_ids)
@@ -9336,6 +9397,7 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9336
  response = self.call_deepseek(
9337
  prompt, json.dumps(payload, ensure_ascii=False), max_tokens=900, temp=0.0,
9338
  model_role='reasoning', thinking=False, allow_fallback=False,
 
9339
  )
9340
  parsed = self._extract_json_from_text(response)
9341
  raw = parsed.get('delta_codes', []) if isinstance(parsed, dict) else []
@@ -9432,6 +9494,7 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9432
  effective_prompt, marked_source,
9433
  max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
9434
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
9435
  )
9436
  parsed = self._parse_marked_translation(result, [block_id])
9437
  try:
@@ -9458,6 +9521,7 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9458
  strict_prompt, marked_source,
9459
  max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
9460
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
9461
  )
9462
  strict_parsed = self._parse_marked_translation(strict_result, [block_id])
9463
  restored = self._restore_translation_tokens(
@@ -9483,6 +9547,7 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9483
  numeric_prompt, marked_source,
9484
  max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
9485
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
 
9486
  )
9487
  retry_parsed = self._parse_marked_translation(retry_result, [block_id])
9488
  restored = self._restore_translation_tokens(
@@ -9856,6 +9921,7 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
9856
  system_prompt, json.dumps(payload, ensure_ascii=False),
9857
  max_tokens=max(1200, min(3200, 600 + 180 * len(model_rows))),
9858
  temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
 
9859
  )
9860
  parsed = self._extract_json_from_text(response)
9861
  returned = parsed.get('items', []) if isinstance(parsed, dict) else []
@@ -10620,6 +10686,7 @@ SOURCE UNIT:
10620
  translated = self.call_model(
10621
  'You are SmartPaper Adaptive Translation. Produce faithful academic translation with no added or omitted claims.',
10622
  prompt, max_tokens=8000, temp=0.1, model_role='translation', thinking=False, allow_fallback=True,
 
10623
  )
10624
  translated = re.sub(r'<think>.*?</think>', '', str(translated or ''), flags=re.S).strip()
10625
  if not translated:
 
272
  self, *, usage, model, role, attempt_index, streaming, thinking,
273
  reasoning_effort, system_prompt, user_prompt, started_at,
274
  completed_at, first_token_at=None, finish_reason="",
275
+ response_payload=None, logical_node="", model_policy_version="",
276
  ):
277
  if not self._model_usage_enabled():
278
  return
 
308
  metadata=sanitize_metadata({
309
  "system_fingerprint": response_payload.get("system_fingerprint"),
310
  "reasoning_effort": reasoning_effort,
311
+ "logical_node": str(logical_node or ""),
312
+ "model_policy_version": str(model_policy_version or ""),
313
+ "resolved_model_id": str(response_payload.get("model") or model or ""),
314
  }),
315
  )
316
  emit_usage_event(getattr(self, "_model_usage_sink", None), event)
 
854
  requested_model,
855
  requested_thinking,
856
  allow_fallback,
857
+ retry_budget=None,
858
  ):
859
+ total = (
860
+ self.deepseek_max_retries
861
+ if retry_budget is None
862
+ else max(1, 1 + int(retry_budget))
863
+ )
864
  fallback_model = self.deepseek_models.get("fallback", "")
865
  for attempt in range(total):
866
  model = requested_model
 
885
  thinking=None,
886
  reasoning_effort=None,
887
  allow_fallback=None,
888
+ response_format=None, retry_budget=None, logical_node=None,
889
+ model_policy_version=None, route_enforced=False,
890
  ):
891
  if not str(getattr(self, "ds_key", "") or "").strip():
892
  raise DeepSeekAuthenticationError(
 
905
  reasoning_effort=reasoning_effort,
906
  allow_fallback=allow_fallback,
907
  )
908
+ if route_enforced:
909
+ resolved_fallback = False
910
+ if not requested_thinking:
911
+ resolved_effort = None
912
+ attempt_total = (
913
+ self.deepseek_max_retries
914
+ if retry_budget is None
915
+ else max(1, 1 + int(retry_budget))
916
+ )
917
  tokens_est = self._count_tokens(str(user_prompt))
918
 
919
  logging.info(
 
931
  requested_model,
932
  requested_thinking,
933
  resolved_fallback,
934
+ retry_budget=retry_budget,
935
  ):
936
  with self.lock:
937
  self.ds_calls += 1
 
961
  logging.info(
962
  "DeepSeek static attempt start | attempt=%s/%s | model=%s",
963
  attempt + 1,
964
+ attempt_total,
965
  current_model,
966
  )
967
  response = requests.post(
 
1019
  completed_at=completed_at,
1020
  finish_reason=choices[0].get("finish_reason"),
1021
  response_payload=result,
1022
+ logical_node=logical_node,
1023
+ model_policy_version=model_policy_version,
1024
  )
1025
  return content
1026
 
 
1037
  DeepSeekAPIError,
1038
  ) as exc:
1039
  last_error = exc
1040
+ is_last = attempt == attempt_total - 1
1041
  logging.error(
1042
  "DeepSeek attempt %s/%s failed: %s",
1043
  attempt + 1,
1044
+ attempt_total,
1045
  exc,
1046
  )
1047
  self._touch_execution_progress("deepseek_static_retry")
 
1051
 
1052
  raise DeepSeekAPIError(
1053
  "DeepSeek API failed after "
1054
+ f"{attempt_total} attempts: {last_error}"
1055
  )
1056
 
1057
  def _stream_deepseek_transport(
 
1065
  reasoning_effort=None,
1066
  temp=0.3,
1067
  allow_fallback=None,
1068
+ reasoning_only_recovery=False, retry_budget=None, logical_node=None,
1069
+ model_policy_version=None, route_enforced=False,
1070
  ):
1071
  if not str(getattr(self, "ds_key", "") or "").strip():
1072
  raise DeepSeekAuthenticationError(
 
1085
  reasoning_effort=reasoning_effort,
1086
  allow_fallback=allow_fallback,
1087
  )
1088
+ if route_enforced:
1089
+ resolved_fallback = False
1090
+ if not requested_thinking:
1091
+ resolved_effort = None
1092
+ attempt_total = (
1093
+ self.deepseek_max_retries
1094
+ if retry_budget is None
1095
+ else max(1, 1 + int(retry_budget))
1096
+ )
1097
  tokens_est = self._count_tokens(str(user_prompt))
1098
 
1099
  logging.info(
 
1111
  requested_model,
1112
  requested_thinking,
1113
  resolved_fallback,
1114
+ retry_budget=retry_budget,
1115
  ):
1116
  with self.lock:
1117
  self.ds_calls += 1
 
1153
  logging.info(
1154
  "DeepSeek stream attempt start | attempt=%s/%s | model=%s | idle_limit=%ss",
1155
  attempt + 1,
1156
+ attempt_total,
1157
  current_model,
1158
  self.deepseek_stream_idle_seconds,
1159
  )
 
1292
  first_token_at=first_content_at,
1293
  finish_reason=stream_finish_reason,
1294
  response_payload=stream_response_payload,
1295
+ logical_node=logical_node,
1296
+ model_policy_version=model_policy_version,
1297
  )
1298
  return
1299
 
 
1319
  logging.error(
1320
  "DeepSeek stream attempt %s/%s failed: %s",
1321
  attempt + 1,
1322
+ attempt_total,
1323
  exc,
1324
  )
1325
 
 
1330
  ) from exc
1331
 
1332
  self._touch_execution_progress("deepseek_stream_retry")
1333
+ is_last = attempt == attempt_total - 1
1334
  if is_last:
1335
  break
1336
  time.sleep(self._retry_delay(attempt))
1337
 
1338
  raise DeepSeekAPIError(
1339
  "DeepSeek streaming failed after "
1340
+ f"{attempt_total} attempts: {last_error}"
1341
  )
1342
 
1343
  def set_model_policy_snapshot(self, snapshot):
1344
  self.model_policy_snapshot = dict(snapshot or {})
1345
  self.model_gateway.set_policy_snapshot(self.model_policy_snapshot)
1346
 
1347
+ def set_trusted_product_policy_snapshot(self, snapshot):
1348
+ self.model_gateway.set_trusted_product_policy_snapshot(snapshot)
1349
+
1350
  def call_model(
1351
  self, system_prompt, user_prompt, max_tokens=8000, temp=0.3,
1352
  model_name=None, model_role="reasoning", thinking=None,
1353
  reasoning_effort=None, allow_fallback=None, response_format=None,
1354
+ logical_node=None,
1355
  ):
1356
  gateway = getattr(self, "model_gateway", None)
1357
  if gateway is None:
 
1364
  response_format=response_format,
1365
  )
1366
  return gateway.call(
1367
+ capability=model_role, logical_node=logical_node,
1368
+ system_prompt=system_prompt, user_prompt=user_prompt,
1369
  max_tokens=max_tokens, temp=temp, model_name=model_name,
1370
  thinking=thinking, reasoning_effort=reasoning_effort,
1371
  allow_fallback=allow_fallback, response_format=response_format,
 
1374
  def stream_model(
1375
  self, system_prompt, user_prompt, max_tokens=8000, model_name=None,
1376
  model_role="reasoning", thinking=None, reasoning_effort=None, temp=0.3,
1377
+ allow_fallback=None, reasoning_only_recovery=False, logical_node=None,
1378
  ):
1379
  gateway = getattr(self, "model_gateway", None)
1380
  if gateway is None:
 
1386
  )
1387
  return
1388
  yield from gateway.stream(
1389
+ capability=model_role, logical_node=logical_node,
1390
+ system_prompt=system_prompt, user_prompt=user_prompt,
1391
  max_tokens=max_tokens, model_name=model_name, thinking=thinking,
1392
  reasoning_effort=reasoning_effort, temp=temp, allow_fallback=allow_fallback,
1393
  reasoning_only_recovery=reasoning_only_recovery,
 
1399
  self, system_prompt, user_prompt, max_tokens=8000, temp=0.3,
1400
  model_name=None, model_role="reasoning", thinking=None,
1401
  reasoning_effort=None, allow_fallback=None, response_format=None,
1402
+ logical_node=None,
1403
  ):
1404
  return self.call_model(
1405
  system_prompt, user_prompt, max_tokens=max_tokens, temp=temp,
1406
  model_name=model_name, model_role=model_role, thinking=thinking,
1407
  reasoning_effort=reasoning_effort, allow_fallback=allow_fallback,
1408
+ response_format=response_format, logical_node=logical_node,
1409
  )
1410
 
1411
  def stream_deepseek(
1412
  self, system_prompt, user_prompt, max_tokens=8000, model_name=None,
1413
  model_role="reasoning", thinking=None, reasoning_effort=None, temp=0.3,
1414
+ allow_fallback=None, reasoning_only_recovery=False, logical_node=None,
1415
  ):
1416
  yield from self.stream_model(
1417
  system_prompt, user_prompt, max_tokens=max_tokens, model_name=model_name,
1418
  model_role=model_role, thinking=thinking, reasoning_effort=reasoning_effort,
1419
  temp=temp, allow_fallback=allow_fallback,
1420
+ reasoning_only_recovery=reasoning_only_recovery, logical_node=logical_node,
1421
  )
1422
 
1423
  def parse_user_core_literature(self, user_lit_text):
 
1439
  model_role="reasoning",
1440
  thinking=True,
1441
  allow_fallback=False,
1442
+ logical_node='literature_parsing',
1443
  )
1444
  papers = self._extract_json_from_text(response)
1445
  if not papers:
 
1503
  user_prompt = APP_CONFIG["prompt_templates"]["synthesis_user"].replace(
1504
  "{topic}", topic).replace("{user_injection}", user_injection).replace(
1505
  "{refs_text}", refs_text).replace("{directive}", APP_CONFIG['deep_synthesis_directive'])
1506
+ return self.call_deepseek(sys_prompt, user_prompt, temp=0.2, logical_node="literature_synthesis")
1507
 
1508
  def generate_anchors(
1509
  self,
 
1546
  model_role="reasoning",
1547
  thinking=False,
1548
  allow_fallback=False,
1549
+ logical_node='literature_synthesis',
1550
  )
1551
 
1552
  return _stream_direct_synthesis()
 
1605
  model_role="reasoning",
1606
  thinking=False,
1607
  allow_fallback=False,
1608
+ logical_node='research_logic',
1609
  )
1610
 
1611
  return _stream_direct_research_logic()
 
1712
  thinking=False,
1713
  allow_fallback=False,
1714
  response_format={"type": "json_object"},
1715
+ logical_node='front_matter_writing',
1716
  )
1717
  try:
1718
  parsed = self._extract_json_from_text(response)
 
2675
  user_prompt,
2676
  temp=0.1 + attempt * 0.1,
2677
  model_role="fast",
2678
+ logical_node='search_jit',
2679
  ))
2680
  if queries and isinstance(queries, list) and len(queries) > 0:
2681
  final_queries = [str(q).strip() for q in queries if self._is_english_dominant(str(q).strip())]
 
3128
  f"Topic: {topic}\nKeywords: {keywords}\nUsed: {sorted(all_used_terms)}",
3129
  temp=0.4,
3130
  model_role='fast',
3131
+ logical_node='search_jit',
3132
  )
3133
  parsed = self._extract_json_from_text(response)
3134
  terms = parsed if isinstance(parsed, list) else []
 
3334
  model_role='reasoning',
3335
  thinking=False,
3336
  allow_fallback=False,
3337
+ logical_node='citation_planning',
3338
  )
3339
  parsed = self._extract_json_from_text(raw)
3340
  parsed_rows = parsed.get('assignments') if isinstance(parsed, dict) else None
 
5291
  model_role='reasoning',
5292
  thinking=False,
5293
  allow_fallback=False,
5294
+ logical_node='critical_citation_adjudication',
5295
  )
5296
  parsed = self._extract_json_from_text(response)
5297
  if isinstance(parsed, dict):
 
5435
  model_role="reasoning",
5436
  thinking=False,
5437
  allow_fallback=False,
5438
+ logical_node='ordinary_citation_qa',
5439
  )
5440
  parsed = self._extract_json_from_text(response)
5441
  last_error = None
 
5489
  model_role="reasoning",
5490
  thinking=False,
5491
  allow_fallback=False,
5492
+ logical_node='ordinary_citation_qa',
5493
  )
5494
  parsed = self._extract_json_from_text(response)
5495
  by_id = index_items(parsed)
 
5962
  model_role='writing',
5963
  thinking=False,
5964
  allow_fallback=False,
5965
+ logical_node='ordinary_repair',
5966
  )
5967
  revised = self._remove_ai_meta_traces(revised)
5968
  original_numbers = self._numeric_tokens(value)
 
6015
  raw = self.call_deepseek(
6016
  'You are an independent senior management-journal reviewer. Be exact and conservative.',
6017
  prompt, max_tokens=800, temp=0.0, model_role='fast', thinking=False, allow_fallback=True,
6018
+ logical_node='section_critique',
6019
  )
6020
  except PipelineCancelledError:
6021
  raise
 
6090
  repaired = self.call_model(
6091
  'You are SmartPaper Adaptive Academic Repair. Preserve evidence truth and repair prose with Conciseness, Accuracy, and Rigor.',
6092
  prompt, max_tokens=7000, temp=0.15, model_role='repair', thinking=True, allow_fallback=True,
6093
+ logical_node='ordinary_repair',
6094
  )
6095
  except PipelineCancelledError:
6096
  raise
 
8113
  json.dumps({'ir_version': self.CANONICAL_GLOSSARY_IR_VERSION, 'terms': payload}, ensure_ascii=False),
8114
  max_tokens=max(1200, min(5000, 120 + len(payload) * 90)),
8115
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
8116
+ logical_node='translation',
8117
  )
8118
  parsed = self._extract_json_from_text(response)
8119
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
 
8376
  json.dumps({'ir_version': self.CANONICAL_GLOSSARY_IR_VERSION, 'terms': payload}, ensure_ascii=False),
8377
  max_tokens=max(1200, min(4200, 180 + len(payload) * 120)),
8378
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
8379
+ logical_node='ordinary_translation_qa',
8380
  )
8381
  parsed = self._extract_json_from_text(response)
8382
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
 
8509
  json.dumps({'terms': missing}, ensure_ascii=False),
8510
  max_tokens=max(1200, min(5000, len(missing) * 80)),
8511
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
8512
+ logical_node='translation',
8513
  )
8514
  parsed = self._extract_json_from_text(response)
8515
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
 
8657
  response = self.call_deepseek(
8658
  'You are a bilingual academic translation equivalence auditor. Be conservative and evidence-preserving.',
8659
  prompt, max_tokens=1200, temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
8660
+ logical_node='ordinary_translation_qa',
8661
  )
8662
  parsed = self._extract_json_from_text(response)
8663
  ok = parsed.get('ok') if isinstance(parsed, dict) else None
 
8749
  system_prompt, json.dumps(payload, ensure_ascii=False),
8750
  max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
8751
  temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
8752
+ logical_node='ordinary_translation_qa',
8753
  )
8754
  parsed = self._extract_json_from_text(response)
8755
  returned = parsed.get('items', []) if isinstance(parsed, dict) else []
 
9021
  model_role='translation',
9022
  thinking=False,
9023
  allow_fallback=False,
9024
+ logical_node='translation',
9025
  )
9026
  parsed = self._extract_json_from_text(response)
9027
  items = parsed.get('terms', []) if isinstance(parsed, dict) else []
 
9211
  effective_prompt, marked_source,
9212
  max_tokens=max(1800, min(9000, self._count_tokens(marked_source) * 2 + 1200)),
9213
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
9214
+ logical_node='translation',
9215
  )
9216
  try:
9217
  parsed = self._parse_marked_translation(result, expected_ids)
 
9397
  response = self.call_deepseek(
9398
  prompt, json.dumps(payload, ensure_ascii=False), max_tokens=900, temp=0.0,
9399
  model_role='reasoning', thinking=False, allow_fallback=False,
9400
+ logical_node='ordinary_translation_qa',
9401
  )
9402
  parsed = self._extract_json_from_text(response)
9403
  raw = parsed.get('delta_codes', []) if isinstance(parsed, dict) else []
 
9494
  effective_prompt, marked_source,
9495
  max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
9496
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
9497
+ logical_node='translation',
9498
  )
9499
  parsed = self._parse_marked_translation(result, [block_id])
9500
  try:
 
9521
  strict_prompt, marked_source,
9522
  max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
9523
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
9524
+ logical_node='translation',
9525
  )
9526
  strict_parsed = self._parse_marked_translation(strict_result, [block_id])
9527
  restored = self._restore_translation_tokens(
 
9547
  numeric_prompt, marked_source,
9548
  max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
9549
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
9550
+ logical_node='translation',
9551
  )
9552
  retry_parsed = self._parse_marked_translation(retry_result, [block_id])
9553
  restored = self._restore_translation_tokens(
 
9921
  system_prompt, json.dumps(payload, ensure_ascii=False),
9922
  max_tokens=max(1200, min(3200, 600 + 180 * len(model_rows))),
9923
  temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
9924
+ logical_node='critical_translation_adjudication',
9925
  )
9926
  parsed = self._extract_json_from_text(response)
9927
  returned = parsed.get('items', []) if isinstance(parsed, dict) else []
 
10686
  translated = self.call_model(
10687
  'You are SmartPaper Adaptive Translation. Produce faithful academic translation with no added or omitted claims.',
10688
  prompt, max_tokens=8000, temp=0.1, model_role='translation', thinking=False, allow_fallback=True,
10689
+ logical_node='translation',
10690
  )
10691
  translated = re.sub(r'<think>.*?</think>', '', str(translated or ''), flags=re.S).strip()
10692
  if not translated:
core/qualitative_engine.py CHANGED
@@ -733,6 +733,7 @@ Rules:
733
  base_prompt,
734
  max_tokens=8000, temp=0.0, model_role="reasoning",
735
  thinking=False, allow_fallback=False, response_format={"type": "json_object"},
 
736
  )
737
  else:
738
  retry_prompt = base_prompt + (
@@ -753,6 +754,7 @@ Rules:
753
  thinking=False,
754
  allow_fallback=False,
755
  response_format={"type": "json_object"},
 
756
  )
757
 
758
  response = re.sub(
 
733
  base_prompt,
734
  max_tokens=8000, temp=0.0, model_role="reasoning",
735
  thinking=False, allow_fallback=False, response_format={"type": "json_object"},
736
+ logical_node='structured_json',
737
  )
738
  else:
739
  retry_prompt = base_prompt + (
 
754
  thinking=False,
755
  allow_fallback=False,
756
  response_format={"type": "json_object"},
757
+ logical_node='structured_json',
758
  )
759
 
760
  response = re.sub(
core/quantitative_engine.py CHANGED
@@ -480,6 +480,7 @@ Keywords: {keywords}"""
480
  model_role="fast",
481
  thinking=False,
482
  allow_fallback=True,
 
483
  )
484
  parsed = agent._extract_json_from_text(raw)
485
  if not isinstance(parsed, dict):
 
480
  model_role="fast",
481
  thinking=False,
482
  allow_fallback=True,
483
+ logical_node='structured_json',
484
  )
485
  parsed = agent._extract_json_from_text(raw)
486
  if not isinstance(parsed, dict):