diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 5880ffc85..79649f1da 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -1320,7 +1320,7 @@ public partial class TaskInfoAgent : TaskInfo
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("activeTimeMs")]
- public TimeSpan? ActiveTimeMs { get; set; }
+ public TimeSpan? ActiveTime { get; set; }
/// Type of agent running this task.
[JsonPropertyName("agentType")]
@@ -2970,7 +2970,7 @@ public sealed class UsageGetMetricsResult
[Range(0, double.MaxValue)]
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonPropertyName("totalApiDurationMs")]
- public TimeSpan TotalApiDurationMs { get; set; }
+ public TimeSpan TotalApiDuration { get; set; }
/// Session-wide accumulated nano-AI units cost.
[Range((double)0, (double)long.MaxValue)]
diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs
index bd00fdc69..189c5104c 100644
--- a/dotnet/src/Generated/SessionEvents.cs
+++ b/dotnet/src/Generated/SessionEvents.cs
@@ -1395,7 +1395,7 @@ public sealed partial class SessionScheduleCreatedData
/// Interval between ticks in milliseconds.
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonPropertyName("intervalMs")]
- public required TimeSpan IntervalMs { get; set; }
+ public required TimeSpan Interval { get; set; }
/// Prompt text that gets enqueued on every tick.
[JsonPropertyName("prompt")]
@@ -1672,7 +1672,7 @@ public sealed partial class SessionShutdownData
/// Cumulative time spent in API calls during the session, in milliseconds.
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonPropertyName("totalApiDurationMs")]
- public required TimeSpan TotalApiDurationMs { get; set; }
+ public required TimeSpan TotalApiDuration { get; set; }
/// Session-wide accumulated nano-AI units cost.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -2157,7 +2157,7 @@ public sealed partial class AssistantUsageData
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("interTokenLatencyMs")]
- public TimeSpan? InterTokenLatencyMs { get; set; }
+ public TimeSpan? InterTokenLatency { get; set; }
/// Model identifier used for this API call.
[JsonPropertyName("model")]
@@ -2199,7 +2199,7 @@ public sealed partial class AssistantUsageData
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("ttftMs")]
- public TimeSpan? TtftMs { get; set; }
+ public TimeSpan? Ttft { get; set; }
}
/// Failed LLM API call metadata for telemetry.
@@ -2214,7 +2214,7 @@ public sealed partial class ModelCallFailureData
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("durationMs")]
- public TimeSpan? DurationMs { get; set; }
+ public TimeSpan? Duration { get; set; }
/// Raw provider/runtime error message for restricted telemetry.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -2464,7 +2464,7 @@ public sealed partial class SubagentCompletedData
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("durationMs")]
- public TimeSpan? DurationMs { get; set; }
+ public TimeSpan? Duration { get; set; }
/// Model used by the sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -2501,7 +2501,7 @@ public sealed partial class SubagentFailedData
[JsonConverter(typeof(MillisecondsTimeSpanConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("durationMs")]
- public TimeSpan? DurationMs { get; set; }
+ public TimeSpan? Duration { get; set; }
/// Error message describing why the sub-agent failed.
[JsonPropertyName("error")]
diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs
index 9e2742deb..3622821dc 100644
--- a/dotnet/test/Unit/SessionEventSerializationTests.cs
+++ b/dotnet/test/Unit/SessionEventSerializationTests.cs
@@ -85,7 +85,7 @@ public class SessionEventSerializationTests
{
ShutdownType = ShutdownType.Routine,
TotalPremiumRequests = 1,
- TotalApiDurationMs = TimeSpan.FromMilliseconds(100),
+ TotalApiDuration = TimeSpan.FromMilliseconds(100),
SessionStartTime = 1773609948932,
CodeChanges = new ShutdownCodeChanges
{
diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts
similarity index 85%
rename from nodejs/test/python-codegen.test.ts
rename to nodejs/test/session-event-codegen.test.ts
index e70f65011..ffb8936e4 100644
--- a/nodejs/test/python-codegen.test.ts
+++ b/nodejs/test/session-event-codegen.test.ts
@@ -6,7 +6,7 @@ import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts";
import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts";
import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts";
-describe("python session event codegen", () => {
+describe("session event codegen", () => {
it("maps special schema formats to the expected Python types", () => {
const schema: JSONSchema7 = {
definitions: {
@@ -86,6 +86,91 @@ describe("python session event codegen", () => {
expect(code).toContain("count: int");
});
+ it("strips Ms suffixes from duration member names while preserving JSON names", () => {
+ const schema: JSONSchema7 = {
+ definitions: {
+ SessionEvent: {
+ anyOf: [
+ {
+ type: "object",
+ required: ["type", "data"],
+ properties: {
+ type: { const: "session.synthetic" },
+ data: {
+ type: "object",
+ required: ["durationMs", "integerDurationMs", "URLMs"],
+ properties: {
+ durationMs: { type: "number", format: "duration" },
+ integerDurationMs: { type: "integer", format: "duration" },
+ optionalDurationMs: {
+ type: ["number", "null"],
+ format: "duration",
+ },
+ nullableDurationMs: {
+ anyOf: [
+ { type: "number", format: "duration" },
+ { type: "null" },
+ ],
+ },
+ URLMs: { type: "number", format: "duration" },
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ };
+
+ const pythonCode = generatePythonSessionEventsCode(schema);
+
+ expect(pythonCode).toContain("duration: timedelta");
+ expect(pythonCode).toContain("integer_duration: timedelta");
+ expect(pythonCode).toContain("optional_duration: timedelta | None = None");
+ expect(pythonCode).toContain("nullable_duration: timedelta | None = None");
+ expect(pythonCode).toContain("urlms: timedelta");
+ expect(pythonCode).toContain('duration = from_timedelta(obj.get("durationMs"))');
+ expect(pythonCode).toContain('result["durationMs"] = to_timedelta(self.duration)');
+ expect(pythonCode).toContain(
+ 'integer_duration = from_timedelta(obj.get("integerDurationMs"))'
+ );
+ expect(pythonCode).toContain(
+ 'result["integerDurationMs"] = to_timedelta_int(self.integer_duration)'
+ );
+ expect(pythonCode).toContain(
+ 'optional_duration = from_union([from_none, from_timedelta], obj.get("optionalDurationMs"))'
+ );
+ expect(pythonCode).toContain(
+ 'result["optionalDurationMs"] = from_union([from_none, to_timedelta], self.optional_duration)'
+ );
+ expect(pythonCode).toContain(
+ 'nullable_duration = from_union([from_none, from_timedelta], obj.get("nullableDurationMs"))'
+ );
+ expect(pythonCode).toContain(
+ 'result["nullableDurationMs"] = from_union([from_none, to_timedelta], self.nullable_duration)'
+ );
+ expect(pythonCode).toContain('urlms = from_timedelta(obj.get("URLMs"))');
+ expect(pythonCode).toContain('result["URLMs"] = to_timedelta(self.urlms)');
+
+ const csharpCode = generateCSharpSessionEventsCode(schema);
+
+ expect(csharpCode).toContain(
+ '[JsonPropertyName("durationMs")]\n public required TimeSpan Duration { get; set; }'
+ );
+ expect(csharpCode).toContain(
+ '[JsonPropertyName("integerDurationMs")]\n public required TimeSpan IntegerDuration { get; set; }'
+ );
+ expect(csharpCode).toContain(
+ '[JsonPropertyName("optionalDurationMs")]\n public TimeSpan? OptionalDuration { get; set; }'
+ );
+ expect(csharpCode).toContain(
+ '[JsonPropertyName("nullableDurationMs")]\n public TimeSpan? NullableDuration { get; set; }'
+ );
+ expect(csharpCode).toContain(
+ '[JsonPropertyName("URLMs")]\n public required TimeSpan URLMs { get; set; }'
+ );
+ });
+
it("collapses redundant callable wrapper lambdas", () => {
const schema: JSONSchema7 = {
definitions: {
diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py
index 056a9ca14..4f1b0bc03 100644
--- a/python/copilot/generated/session_events.py
+++ b/python/copilot/generated/session_events.py
@@ -685,7 +685,7 @@ class AssistantUsageData:
duration: timedelta | None = None
initiator: str | None = None
input_tokens: float | None = None
- inter_token_latency_ms: timedelta | None = None
+ inter_token_latency: timedelta | None = None
output_tokens: float | None = None
# Deprecated: this field is deprecated.
parent_tool_call_id: str | None = None
@@ -693,7 +693,7 @@ class AssistantUsageData:
quota_snapshots: dict[str, AssistantUsageQuotaSnapshot] | None = None
reasoning_effort: str | None = None
reasoning_tokens: float | None = None
- ttft_ms: timedelta | None = None
+ ttft: timedelta | None = None
@staticmethod
def from_dict(obj: Any) -> "AssistantUsageData":
@@ -708,14 +708,14 @@ def from_dict(obj: Any) -> "AssistantUsageData":
duration = from_union([from_none, from_timedelta], obj.get("duration"))
initiator = from_union([from_none, from_str], obj.get("initiator"))
input_tokens = from_union([from_none, from_float], obj.get("inputTokens"))
- inter_token_latency_ms = from_union([from_none, from_timedelta], obj.get("interTokenLatencyMs"))
+ inter_token_latency = from_union([from_none, from_timedelta], obj.get("interTokenLatencyMs"))
output_tokens = from_union([from_none, from_float], obj.get("outputTokens"))
parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId"))
provider_call_id = from_union([from_none, from_str], obj.get("providerCallId"))
quota_snapshots = from_union([from_none, lambda x: from_dict(AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots"))
reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort"))
reasoning_tokens = from_union([from_none, from_float], obj.get("reasoningTokens"))
- ttft_ms = from_union([from_none, from_timedelta], obj.get("ttftMs"))
+ ttft = from_union([from_none, from_timedelta], obj.get("ttftMs"))
return AssistantUsageData(
model=model,
api_call_id=api_call_id,
@@ -727,14 +727,14 @@ def from_dict(obj: Any) -> "AssistantUsageData":
duration=duration,
initiator=initiator,
input_tokens=input_tokens,
- inter_token_latency_ms=inter_token_latency_ms,
+ inter_token_latency=inter_token_latency,
output_tokens=output_tokens,
parent_tool_call_id=parent_tool_call_id,
provider_call_id=provider_call_id,
quota_snapshots=quota_snapshots,
reasoning_effort=reasoning_effort,
reasoning_tokens=reasoning_tokens,
- ttft_ms=ttft_ms,
+ ttft=ttft,
)
def to_dict(self) -> dict:
@@ -758,8 +758,8 @@ def to_dict(self) -> dict:
result["initiator"] = from_union([from_none, from_str], self.initiator)
if self.input_tokens is not None:
result["inputTokens"] = from_union([from_none, to_float], self.input_tokens)
- if self.inter_token_latency_ms is not None:
- result["interTokenLatencyMs"] = from_union([from_none, to_timedelta], self.inter_token_latency_ms)
+ if self.inter_token_latency is not None:
+ result["interTokenLatencyMs"] = from_union([from_none, to_timedelta], self.inter_token_latency)
if self.output_tokens is not None:
result["outputTokens"] = from_union([from_none, to_float], self.output_tokens)
if self.parent_tool_call_id is not None:
@@ -772,8 +772,8 @@ def to_dict(self) -> dict:
result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort)
if self.reasoning_tokens is not None:
result["reasoningTokens"] = from_union([from_none, to_float], self.reasoning_tokens)
- if self.ttft_ms is not None:
- result["ttftMs"] = from_union([from_none, to_timedelta], self.ttft_ms)
+ if self.ttft is not None:
+ result["ttftMs"] = from_union([from_none, to_timedelta], self.ttft)
return result
@@ -1751,7 +1751,7 @@ class ModelCallFailureData:
"Failed LLM API call metadata for telemetry"
source: ModelCallFailureSource
api_call_id: str | None = None
- duration_ms: timedelta | None = None
+ duration: timedelta | None = None
error_message: str | None = None
initiator: str | None = None
model: str | None = None
@@ -1763,7 +1763,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData":
assert isinstance(obj, dict)
source = parse_enum(ModelCallFailureSource, obj.get("source"))
api_call_id = from_union([from_none, from_str], obj.get("apiCallId"))
- duration_ms = from_union([from_none, from_timedelta], obj.get("durationMs"))
+ duration = from_union([from_none, from_timedelta], obj.get("durationMs"))
error_message = from_union([from_none, from_str], obj.get("errorMessage"))
initiator = from_union([from_none, from_str], obj.get("initiator"))
model = from_union([from_none, from_str], obj.get("model"))
@@ -1772,7 +1772,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData":
return ModelCallFailureData(
source=source,
api_call_id=api_call_id,
- duration_ms=duration_ms,
+ duration=duration,
error_message=error_message,
initiator=initiator,
model=model,
@@ -1785,8 +1785,8 @@ def to_dict(self) -> dict:
result["source"] = to_enum(ModelCallFailureSource, self.source)
if self.api_call_id is not None:
result["apiCallId"] = from_union([from_none, from_str], self.api_call_id)
- if self.duration_ms is not None:
- result["durationMs"] = from_union([from_none, to_timedelta], self.duration_ms)
+ if self.duration is not None:
+ result["durationMs"] = from_union([from_none, to_timedelta], self.duration)
if self.error_message is not None:
result["errorMessage"] = from_union([from_none, from_str], self.error_message)
if self.initiator is not None:
@@ -3046,7 +3046,7 @@ def to_dict(self) -> dict:
class SessionScheduleCreatedData:
"Scheduled prompt registered via /every or /after"
id: int
- interval_ms: timedelta
+ interval: timedelta
prompt: str
display_prompt: str | None = None
recurring: bool | None = None
@@ -3055,13 +3055,13 @@ class SessionScheduleCreatedData:
def from_dict(obj: Any) -> "SessionScheduleCreatedData":
assert isinstance(obj, dict)
id = from_int(obj.get("id"))
- interval_ms = from_timedelta(obj.get("intervalMs"))
+ interval = from_timedelta(obj.get("intervalMs"))
prompt = from_str(obj.get("prompt"))
display_prompt = from_union([from_none, from_str], obj.get("displayPrompt"))
recurring = from_union([from_none, from_bool], obj.get("recurring"))
return SessionScheduleCreatedData(
id=id,
- interval_ms=interval_ms,
+ interval=interval,
prompt=prompt,
display_prompt=display_prompt,
recurring=recurring,
@@ -3070,7 +3070,7 @@ def from_dict(obj: Any) -> "SessionScheduleCreatedData":
def to_dict(self) -> dict:
result: dict = {}
result["id"] = to_int(self.id)
- result["intervalMs"] = to_timedelta_int(self.interval_ms)
+ result["intervalMs"] = to_timedelta_int(self.interval)
result["prompt"] = from_str(self.prompt)
if self.display_prompt is not None:
result["displayPrompt"] = from_union([from_none, from_str], self.display_prompt)
@@ -3086,7 +3086,7 @@ class SessionShutdownData:
model_metrics: dict[str, ShutdownModelMetric]
session_start_time: float
shutdown_type: ShutdownType
- total_api_duration_ms: timedelta
+ total_api_duration: timedelta
total_premium_requests: float
conversation_tokens: float | None = None
current_model: str | None = None
@@ -3104,7 +3104,7 @@ def from_dict(obj: Any) -> "SessionShutdownData":
model_metrics = from_dict(ShutdownModelMetric.from_dict, obj.get("modelMetrics"))
session_start_time = from_float(obj.get("sessionStartTime"))
shutdown_type = parse_enum(ShutdownType, obj.get("shutdownType"))
- total_api_duration_ms = from_timedelta(obj.get("totalApiDurationMs"))
+ total_api_duration = from_timedelta(obj.get("totalApiDurationMs"))
total_premium_requests = from_float(obj.get("totalPremiumRequests"))
conversation_tokens = from_union([from_none, from_float], obj.get("conversationTokens"))
current_model = from_union([from_none, from_str], obj.get("currentModel"))
@@ -3119,7 +3119,7 @@ def from_dict(obj: Any) -> "SessionShutdownData":
model_metrics=model_metrics,
session_start_time=session_start_time,
shutdown_type=shutdown_type,
- total_api_duration_ms=total_api_duration_ms,
+ total_api_duration=total_api_duration,
total_premium_requests=total_premium_requests,
conversation_tokens=conversation_tokens,
current_model=current_model,
@@ -3137,7 +3137,7 @@ def to_dict(self) -> dict:
result["modelMetrics"] = from_dict(lambda x: to_class(ShutdownModelMetric, x), self.model_metrics)
result["sessionStartTime"] = to_float(self.session_start_time)
result["shutdownType"] = to_enum(ShutdownType, self.shutdown_type)
- result["totalApiDurationMs"] = to_timedelta(self.total_api_duration_ms)
+ result["totalApiDurationMs"] = to_timedelta(self.total_api_duration)
result["totalPremiumRequests"] = to_float(self.total_premium_requests)
if self.conversation_tokens is not None:
result["conversationTokens"] = from_union([from_none, to_float], self.conversation_tokens)
@@ -3728,7 +3728,7 @@ class SubagentCompletedData:
agent_display_name: str
agent_name: str
tool_call_id: str
- duration_ms: timedelta | None = None
+ duration: timedelta | None = None
model: str | None = None
total_tokens: float | None = None
total_tool_calls: float | None = None
@@ -3739,7 +3739,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData":
agent_display_name = from_str(obj.get("agentDisplayName"))
agent_name = from_str(obj.get("agentName"))
tool_call_id = from_str(obj.get("toolCallId"))
- duration_ms = from_union([from_none, from_timedelta], obj.get("durationMs"))
+ duration = from_union([from_none, from_timedelta], obj.get("durationMs"))
model = from_union([from_none, from_str], obj.get("model"))
total_tokens = from_union([from_none, from_float], obj.get("totalTokens"))
total_tool_calls = from_union([from_none, from_float], obj.get("totalToolCalls"))
@@ -3747,7 +3747,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData":
agent_display_name=agent_display_name,
agent_name=agent_name,
tool_call_id=tool_call_id,
- duration_ms=duration_ms,
+ duration=duration,
model=model,
total_tokens=total_tokens,
total_tool_calls=total_tool_calls,
@@ -3758,8 +3758,8 @@ def to_dict(self) -> dict:
result["agentDisplayName"] = from_str(self.agent_display_name)
result["agentName"] = from_str(self.agent_name)
result["toolCallId"] = from_str(self.tool_call_id)
- if self.duration_ms is not None:
- result["durationMs"] = from_union([from_none, to_timedelta], self.duration_ms)
+ if self.duration is not None:
+ result["durationMs"] = from_union([from_none, to_timedelta], self.duration)
if self.model is not None:
result["model"] = from_union([from_none, from_str], self.model)
if self.total_tokens is not None:
@@ -3788,7 +3788,7 @@ class SubagentFailedData:
agent_name: str
error: str
tool_call_id: str
- duration_ms: timedelta | None = None
+ duration: timedelta | None = None
model: str | None = None
total_tokens: float | None = None
total_tool_calls: float | None = None
@@ -3800,7 +3800,7 @@ def from_dict(obj: Any) -> "SubagentFailedData":
agent_name = from_str(obj.get("agentName"))
error = from_str(obj.get("error"))
tool_call_id = from_str(obj.get("toolCallId"))
- duration_ms = from_union([from_none, from_timedelta], obj.get("durationMs"))
+ duration = from_union([from_none, from_timedelta], obj.get("durationMs"))
model = from_union([from_none, from_str], obj.get("model"))
total_tokens = from_union([from_none, from_float], obj.get("totalTokens"))
total_tool_calls = from_union([from_none, from_float], obj.get("totalToolCalls"))
@@ -3809,7 +3809,7 @@ def from_dict(obj: Any) -> "SubagentFailedData":
agent_name=agent_name,
error=error,
tool_call_id=tool_call_id,
- duration_ms=duration_ms,
+ duration=duration,
model=model,
total_tokens=total_tokens,
total_tool_calls=total_tool_calls,
@@ -3821,8 +3821,8 @@ def to_dict(self) -> dict:
result["agentName"] = from_str(self.agent_name)
result["error"] = from_str(self.error)
result["toolCallId"] = from_str(self.tool_call_id)
- if self.duration_ms is not None:
- result["durationMs"] = from_union([from_none, to_timedelta], self.duration_ms)
+ if self.duration is not None:
+ result["durationMs"] = from_union([from_none, to_timedelta], self.duration)
if self.model is not None:
result["model"] = from_union([from_none, from_str], self.model)
if self.total_tokens is not None:
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index 354a5eda8..199a79913 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -217,6 +217,17 @@ function toPascalCase(name: string): string {
return name.charAt(0).toUpperCase() + name.slice(1);
}
+function stripDurationMillisecondsSuffix(name: string): string {
+ if (name.length > 2 && name.endsWith("Ms") && /[a-z]/.test(name.charAt(name.length - 3))) {
+ return name.slice(0, -2);
+ }
+ return name;
+}
+
+function toCSharpPropertyName(propName: string, schema: JSONSchema7): string {
+ return toPascalCase(isDurationProperty(schema) ? stripDurationMillisecondsSuffix(propName) : propName);
+}
+
function typeToClassName(typeName: string): string {
return splitCSharpIdentifierParts(typeName).map(toPascalCasePart).join("");
}
@@ -441,6 +452,11 @@ function emitDataAnnotations(schema: JSONSchema7, indent: string): string[] {
* milliseconds-based JSON converter rather than expecting ISO 8601 strings.
*/
function isDurationProperty(schema: JSONSchema7): boolean {
+ const nullableInner = getNullableInner(schema);
+ if (nullableInner) {
+ return isDurationProperty(nullableInner);
+ }
+
if (schema.format === "duration") {
const t = schema.type;
if (t === "number" || t === "integer") return true;
@@ -736,7 +752,7 @@ function generateFlattenedBooleanDiscriminatedClass(
const propertyEntries = Array.from(flattenedProperties.entries()).sort(([a], [b]) => a.localeCompare(b));
for (const [propName, info] of propertyEntries) {
const isReq = info.variantCount === variants.length && info.requiredCount === variants.length;
- const csharpName = toPascalCase(propName);
+ const csharpName = toCSharpPropertyName(propName, info.schema);
const csharpType = resolver(info.schema, renamedBase, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
lines.push("");
@@ -839,13 +855,14 @@ function generateDerivedClass(
if (propName === discriminatorProperty) continue;
const isReq = required.has(propName);
- const csharpName = toPascalCase(propName);
- const csharpType = propertyResolver(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
-
- lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " "));
- lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " "));
- if (isSchemaDeprecated(propSchema as JSONSchema7)) pushObsoleteAttributes(lines, " ");
- if (isDurationProperty(propSchema as JSONSchema7)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`);
+ const prop = propSchema as JSONSchema7;
+ const csharpName = toCSharpPropertyName(propName, prop);
+ const csharpType = propertyResolver(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
+
+ lines.push(...xmlDocPropertyComment(prop.description, propName, " "));
+ lines.push(...emitDataAnnotations(prop, " "));
+ if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " ");
+ if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`);
if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`);
lines.push(` [JsonPropertyName("${propName}")]`);
const reqMod = isReq && !csharpType.endsWith("?") ? "required " : "";
@@ -1064,7 +1081,7 @@ function generateNestedClass(
if (typeof propSchema !== "object") continue;
const prop = propSchema as JSONSchema7;
const isReq = required.has(propName);
- const csharpName = toPascalCase(propName);
+ const csharpName = toCSharpPropertyName(propName, prop);
const csharpType = resolveSessionPropertyType(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
lines.push(...xmlDocPropertyComment(prop.description, propName, " "));
@@ -1206,13 +1223,14 @@ function generateDataClass(variant: EventVariant, knownTypes: Map a.localeCompare(b))) {
if (typeof propSchema !== "object") continue;
const isReq = required.has(propName);
- const csharpName = toPascalCase(propName);
- const csharpType = resolveSessionPropertyType(propSchema as JSONSchema7, variant.dataClassName, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
+ const prop = propSchema as JSONSchema7;
+ const csharpName = toCSharpPropertyName(propName, prop);
+ const csharpType = resolveSessionPropertyType(prop, variant.dataClassName, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
- lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " "));
- lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " "));
- if (isSchemaDeprecated(propSchema as JSONSchema7)) pushObsoleteAttributes(lines, " ");
- if (isDurationProperty(propSchema as JSONSchema7)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`);
+ lines.push(...xmlDocPropertyComment(prop.description, propName, " "));
+ lines.push(...emitDataAnnotations(prop, " "));
+ if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " ");
+ if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`);
if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`);
lines.push(` [JsonPropertyName("${propName}")]`);
const reqMod = isReq && !csharpType.endsWith("?") ? "required " : "";
@@ -1229,7 +1247,7 @@ function emitSessionEventEnvelopeProperty(
nestedClasses: Map,
enumOutput: string[]
): string[] {
- const csharpName = toPascalCase(property.name);
+ const csharpName = toCSharpPropertyName(property.name, property.schema);
const csharpType = resolveSessionPropertyType(
property.schema,
"SessionEvent",
@@ -1591,7 +1609,7 @@ function emitRpcClass(
if (typeof propSchema !== "object") continue;
const prop = propSchema as JSONSchema7;
const isReq = requiredSet.has(propName);
- const csharpName = toPascalCase(propName);
+ const csharpName = toCSharpPropertyName(propName, prop);
const csharpType = resolveRpcType(prop, isReq, className, csharpName, extraClasses);
lines.push(...xmlDocPropertyComment(prop.description, propName, " "));
@@ -1790,11 +1808,14 @@ function emitServerInstanceMethod(
if (typeof pSchema !== "object") continue;
const isReq = requiredSet.has(pName);
const jsonSchema = pSchema as JSONSchema7;
+ const csharpName = requestClassName
+ ? toCSharpPropertyName(pName, jsonSchema)
+ : toPascalCase(pName);
const csType = requestClassName
- ? resolveRpcType(jsonSchema, isReq, requestClassName, toPascalCase(pName), classes)
+ ? resolveRpcType(jsonSchema, isReq, requestClassName, csharpName, classes)
: schemaTypeToCSharp(jsonSchema, isReq, rpcKnownTypes);
sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`);
- bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`);
+ bodyAssignments.push(`${csharpName} = ${pName}`);
if (requiresArgumentNullCheck(csType, isReq)) {
argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`);
}
@@ -1938,16 +1959,19 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas
if (useRequestParameter) {
sigParams.push(`${requestClassName}? request = null`);
parameterDescriptions.push({ name: "request", description: rpcParamsDescription(method, effectiveParams) });
- for (const [pName] of paramEntries) {
- bodyAssignments.push(`${toPascalCase(pName)} = request?.${toPascalCase(pName)}`);
+ for (const [pName, pSchema] of paramEntries) {
+ if (typeof pSchema !== "object") continue;
+ const csharpName = toCSharpPropertyName(pName, pSchema as JSONSchema7);
+ bodyAssignments.push(`${csharpName} = request?.${csharpName}`);
}
} else {
for (const [pName, pSchema] of paramEntries) {
if (typeof pSchema !== "object") continue;
const isReq = requiredSet.has(pName);
- const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes);
+ const csharpName = toCSharpPropertyName(pName, pSchema as JSONSchema7);
+ const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, csharpName, classes);
sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`);
- bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`);
+ bodyAssignments.push(`${csharpName} = ${pName}`);
if (requiresArgumentNullCheck(csType, isReq)) {
argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`);
}
diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts
index d69e7fcc9..aaf151282 100644
--- a/scripts/codegen/python.ts
+++ b/scripts/codegen/python.ts
@@ -647,6 +647,57 @@ function toSnakeCase(s: string): string {
.toLowerCase();
}
+function stripDurationMillisecondsSuffix(name: string): string {
+ if (name.length > 2 && name.endsWith("Ms") && /[a-z]/.test(name.charAt(name.length - 3))) {
+ return name.slice(0, -2);
+ }
+ return name;
+}
+
+function isPyDurationProperty(propSchema: JSONSchema7, ctx: PyCodegenCtx): boolean {
+ if (propSchema.$ref && typeof propSchema.$ref === "string") {
+ const resolved = resolveSchema(propSchema, ctx.definitions);
+ if (resolved && resolved !== propSchema) {
+ return isPyDurationProperty(resolved, ctx);
+ }
+ }
+
+ if (propSchema.allOf && propSchema.allOf.length === 1 && typeof propSchema.allOf[0] === "object") {
+ return isPyDurationProperty(propSchema.allOf[0] as JSONSchema7, ctx);
+ }
+
+ if (propSchema.anyOf) {
+ const variants = (propSchema.anyOf as JSONSchema7[])
+ .filter((item) => typeof item === "object")
+ .map(
+ (item) =>
+ resolveSchema(item as JSONSchema7, ctx.definitions) ??
+ (item as JSONSchema7)
+ );
+ const nonNull = variants.filter((item) => !isPyNullLikeSchema(item));
+ return nonNull.length === 1 && isPyDurationProperty(nonNull[0], ctx);
+ }
+
+ if (propSchema.format !== "duration") {
+ return false;
+ }
+
+ const type = propSchema.type;
+ if (type === "number" || type === "integer") {
+ return true;
+ }
+ if (Array.isArray(type)) {
+ const nonNullTypes = type.filter((value) => value !== "null");
+ return nonNullTypes.length === 1 && (nonNullTypes[0] === "number" || nonNullTypes[0] === "integer");
+ }
+
+ return false;
+}
+
+function toPyFieldName(propName: string, propSchema: JSONSchema7, ctx: PyCodegenCtx): string {
+ return toSnakeCase(isPyDurationProperty(propSchema, ctx) ? stripDurationMillisecondsSuffix(propName) : propName);
+}
+
function toPascalCase(s: string): string {
return s
.split(/[._]/)
@@ -952,7 +1003,7 @@ function getPySharedEventEnvelopeProperties(schema: JSONSchema7, ctx: PyCodegenC
return {
...property,
jsonName: name,
- fieldName: toSnakeCase(name),
+ fieldName: toPyFieldName(name, schema, ctx),
required,
hasDefault: !required || resolved.annotation.includes(" | None"),
resolved,
@@ -1473,7 +1524,7 @@ function emitPyClass(
const resolved = resolvePyPropertyType(propSchema, typeName, propName, isRequired, ctx);
return {
jsonName: propName,
- fieldName: toSnakeCase(propName),
+ fieldName: toPyFieldName(propName, propSchema, ctx),
isRequired,
resolved,
};
@@ -1631,7 +1682,7 @@ function emitPyFlatDiscriminatedUnion(
return {
jsonName: propName,
- fieldName: toSnakeCase(propName),
+ fieldName: toPyFieldName(propName, propSchema, ctx),
isRequired: requiredInAll,
resolved,
};