From da4fbee3e3e4646859e97b970bed225a53f48f09 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Fri, 17 Jul 2026 18:20:59 +0530 Subject: [PATCH 01/44] align SDK public APIs to Swift source of truth; add per-SDK usage docs --- .../docs/PublicApiFlutter.dart | 238 ++++++++++++ .../docs/PublicApiKotlin.kt | 229 +++++++++++ .../docs/PublicApiReactNative.ts | 224 +++++++++++ .../docs/PublicApiSwift.swift | 226 +++++++++++ sdk/runanywhere-commons/docs/PublicApiWeb.ts | 243 ++++++++++++ sdk/runanywhere-commons/docs/md/flutter.md | 146 +++++++ sdk/runanywhere-commons/docs/md/kotlin.md | 155 ++++++++ .../docs/md/react-native.md | 136 +++++++ sdk/runanywhere-commons/docs/md/swift.md | 155 ++++++++ sdk/runanywhere-commons/docs/md/web.md | 145 +++++++ .../runanywhere/lib/public/runanywhere.dart | 263 ++++++++++++- .../public/extensions/LLM/RunAnywhereLoRA.kt | 11 + .../LLM/RunAnywhereWebSearchTool.kt | 364 ++++++++++++++++++ .../VLM/RunAnywhereVisionLanguage.kt | 11 + .../core/src/Public/Events/EventBus.ts | 4 +- .../Events/RunAnywhere+SDKEvents.ts | 26 +- .../Public/Extensions/LLM/RunAnywhere+LoRA.ts | 19 +- .../Models/RunAnywhere+ModelRegistry.ts | 19 - .../Extensions/Storage/RunAnywhere+Storage.ts | 7 +- .../VLM/RunAnywhere+VisionLanguage.ts | 23 +- .../packages/core/src/Public/RunAnywhere.ts | 53 ++- .../Bridge/Extensions/CppBridge+RAG.swift | 35 +- .../Extensions/RAG/RunAnywhere+RAG.swift | 10 + .../Extensions/RunAnywhere+FlatFacade.ts | 86 ++++- .../Extensions/RunAnywhere+ModelRegistry.ts | 29 +- .../src/Public/Extensions/RunAnywhere+TTS.ts | 19 + .../src/Public/Extensions/RunAnywhere+VAD.ts | 13 + .../packages/core/src/Public/RunAnywhere.ts | 14 +- 28 files changed, 2808 insertions(+), 95 deletions(-) create mode 100644 sdk/runanywhere-commons/docs/PublicApiFlutter.dart create mode 100644 sdk/runanywhere-commons/docs/PublicApiKotlin.kt create mode 100644 sdk/runanywhere-commons/docs/PublicApiReactNative.ts create mode 100644 sdk/runanywhere-commons/docs/PublicApiSwift.swift create mode 100644 sdk/runanywhere-commons/docs/PublicApiWeb.ts create mode 100644 sdk/runanywhere-commons/docs/md/flutter.md create mode 100644 sdk/runanywhere-commons/docs/md/kotlin.md create mode 100644 sdk/runanywhere-commons/docs/md/react-native.md create mode 100644 sdk/runanywhere-commons/docs/md/swift.md create mode 100644 sdk/runanywhere-commons/docs/md/web.md create mode 100644 sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt diff --git a/sdk/runanywhere-commons/docs/PublicApiFlutter.dart b/sdk/runanywhere-commons/docs/PublicApiFlutter.dart new file mode 100644 index 0000000000..88b79738b9 --- /dev/null +++ b/sdk/runanywhere-commons/docs/PublicApiFlutter.dart @@ -0,0 +1,238 @@ +/* + * PUBLIC API SNAPSHOT — Flutter SDK (sdk/runanywhere-flutter/packages/runanywhere) + * Audit reference only. NOT compilable. Compare against PublicApiSwift.swift (same 16 areas). + * Entry: `abstract final class RunAnywhere` (lib/public/runanywhere.dart) + capability + * objects (RunAnywhere.llm / .stt / .tts / .vad / .vlm / .rag / .lora / .voice / .models / + * .downloads / .solutions / .pluginLoader) and extension classes. + * + * Markers: + * [MISSING] Swift has the feature, Flutter has NO equivalent (real gap) + * [NO-FLAT] behavior exists on a capability object, but Swift's flat RunAnywhere.* static is absent (namespacing gap, not missing behavior) + * [FLUTTER-ONLY] Flutter has it, Swift does not + * [DIVERGE] same feature, different name/shape + * + * HEADLINE: the ONLY truly-absent feature vs Swift is the web-search tool (area 2). + * Everything else is flat-alias namespacing or Dart naming conventions. + */ + +// ===================================================================== +// 1. INIT / LIFECYCLE (static on RunAnywhere) +// ===================================================================== +bool get isInitialized; +bool get isActive; +bool get areServicesReady; +bool get hasCompletedHTTPSetup; // [FLUTTER-ONLY] +String get deviceId; // [DIVERGE] Swift method deviceId(){throws}; Flutter throwing getter +String? get userId; // [DIVERGE] Swift getUserId() +String? get organizationId; // [DIVERGE] Swift getOrganizationId() +bool get isAuthenticated; +bool get isDeviceRegistered; // [DIVERGE] Swift isDeviceRegistered() method +void setHfToken(String? token); +SDKInitParams? get initParams; // [FLUTTER-ONLY] +SDKEnvironment? get environment; +String get version; +EventBus get events; +Future initialize({String? apiKey, String? baseURL, SDKEnvironment environment}); +Future initializeWithParams(SDKInitParams params); // [DIVERGE] Swift 2nd initialize overload +Future completeServicesInitialization(); +Future reset(); + +// ===================================================================== +// 2. LLM +// ===================================================================== +Future generate(String prompt, [LLMGenerationOptions? options]); +Future generateRequest(LLMGenerateRequest request); // [DIVERGE] Swift generate(request) overload +Stream generateStream(String prompt, [LLMGenerationOptions? options]); +Stream generateStreamRequest(LLMGenerateRequest request); // [DIVERGE] Swift generateStream(request) overload +void cancelGeneration(); +Future aggregateStream({required String prompt, required Stream events, Future Function(String)? onToken}); +// llm.chat(String) // [FLUTTER-ONLY] + +// --- Structured output --- +Future generateStructured({required String prompt, required JSONSchema schema, LLMGenerationOptions? options}); +Future generateWithStructuredOutput({required String prompt, required StructuredOutputOptions structuredOutput, LLMGenerationOptions? options}); +StructuredOutputResult extractStructuredOutput({required String text, required JSONSchema schema}); +Stream generateStructuredStream({required String prompt, required JSONSchema schema, LLMGenerationOptions? options}); + +// --- Tool calling (RunAnywhere.tools + flat) --- +void registerTool(ToolDefinition definition, ToolExecutor executor); +void unregisterTool(String name); +List getRegisteredTools(); +void clearTools(); +Future executeTool(ToolCall call); // [DIVERGE] capability method is tools.execute() +Future generateWithTools(String prompt, {LLMGenerationOptions? llmOptions, ToolCallingOptions? options, ToolChoiceMode? toolChoice, String? forcedToolName, bool? validateCalls, List history}); + +// --- Web search --- +// [MISSING] webSearchToolDefinition +// [MISSING] registerWebSearchTool() + +// ===================================================================== +// 3. STT +// ===================================================================== +Future transcribe(Uint8List audio, [STTOptions? options]); +Stream transcribeStream(Stream audio, {STTOptions? options}); +// FLUTTER-ONLY: transcribeBuffer, processStreamingAudio, stopStreamingTranscription, isStreaming + +// ===================================================================== +// 4. TTS +// ===================================================================== +Future synthesize(String text, [TTSOptions? options]); +Stream synthesizeStream(String text, {TTSOptions? options}); // [NO-FLAT] only via RunAnywhere.tts +Future stopSynthesis(); +Future speak(String text, [TTSOptions? options]); +Future stopSpeaking(); // [NO-FLAT] only via RunAnywhere.tts +// FLUTTER-ONLY: isSpeaking, playbackStateStream, playbackProgressStream, availableVoices() + +// ===================================================================== +// 5. VAD (RunAnywhere.vad only — no flat statics) +// ===================================================================== +Future detectVoiceActivity(Uint8List audio, [VADOptions? options]); // [NO-FLAT] +Stream streamVAD(Stream audio); // [NO-FLAT] +void reset(); // [NO-FLAT][DIVERGE] Swift resetVAD() + +// ===================================================================== +// 6. VLM (RunAnywhere.vlm / .visionLanguage — no flat statics) +// ===================================================================== +Future processImage(VLMImage image, {String? prompt, VLMGenerationOptions? options}); // [NO-FLAT] +Stream processImageStream(VLMImage image, {String? prompt, VLMGenerationOptions? options}); // [NO-FLAT][DIVERGE] Swift 2 overloads collapsed into named {prompt,options} +Future cancelVLMGeneration(); // [NO-FLAT] + +// ===================================================================== +// 7. DIFFUSION (Apple-only; fails closed off-Apple) — FULL PARITY +// ===================================================================== +Future generateImage(DiffusionGenerationOptions options); +Stream generateImageStream(DiffusionGenerationOptions options); +Future cancelImageGeneration(); + +// ===================================================================== +// 8. EMBEDDINGS (RunAnywhere.embeddings) +// ===================================================================== +bool get isLoaded; +String? get currentModelId; // [DIVERGE] Swift currentModelID (casing) +Future embed(String text, {required String modelId, EmbeddingsOptions? options}); // [DIVERGE] requires modelId + self-loads +Future embedBatch(EmbeddingsRequest request, {required String modelId}); // [DIVERGE] Swift takes [String] +Future unload(); + +// ===================================================================== +// 9. RAG — FULL PARITY (ragCancelQuery correctly absent, matching Swift) +// ===================================================================== +Future ragResolvedConfiguration({required ModelInfo embeddingModel, required ModelInfo llmModel, RAGConfiguration? baseConfiguration}); +Future ragCreatePipeline(RAGConfiguration config); +Future ragCreatePipelineForModels({required ModelInfo embeddingModel, required ModelInfo llmModel, RAGConfiguration? baseConfiguration}); // [DIVERGE] Swift ragCreatePipeline(embeddingModel:llmModel:) overload +Future ragDestroyPipeline(); +Future ragIngest(RAGDocument document); +Future ragAddDocumentsBatch(List documents); +Future ragClearDocuments(); +Future ragGetDocumentCount(); +Future get ragDocumentCount; +Future ragGetStatistics(); +Future ragQuery(RAGQueryOptions options); +Stream ragQueryStream(RAGQueryOptions options); +// FLUTTER-ONLY ergonomic aliases: query(question), queryStream(question), createPipeline, destroyPipeline, documentCount, getStatistics, clearDocuments + +// ===================================================================== +// 10. LoRA (RunAnywhere.lora) — FULL PARITY +// ===================================================================== +Future apply(LoRAApplyRequest request); +Future applyCatalogAdapter(LoraAdapterCatalogEntry entry, {String? localPath, double? scale, bool replaceExisting}); +Future remove(LoRARemoveRequest request); +Future list(); +Future state(); +Future checkCompatibility(LoRAAdapterConfig config); +Future register(LoraAdapterCatalogEntry entry); +Future registerArtifact(LoraAdapterCatalogEntry entry); +Future download(LoraAdapterCatalogEntry entry, {void Function(double)? onProgress}); +Future listCatalog([LoraAdapterCatalogListRequest? request]); +Future queryCatalog(LoraAdapterCatalogQuery query); +Future getCatalogEntry(LoraAdapterCatalogGetRequest request); +Future markDownloadCompleted(LoraAdapterDownloadCompletedRequest request); +Future markImportCompleted(LoraAdapterDownloadCompletedRequest request); +Future importAdapter(String sourcePath); +Future> adaptersForModel(String modelId); +Future> allRegistered(); + +// ===================================================================== +// 11. VOICE AGENT (RunAnywhere.voice) +// ===================================================================== +String get defaultVADModelID; +Future ensureDefaultVAD({String? modelID}); // [NO-FLAT] +Future initializeVoiceAgent(VoiceAgentComposeConfig config); // [NO-FLAT] +Future componentStates(); // [NO-FLAT][DIVERGE] Swift getVoiceAgentComponentStates() +Future initializeWithLoadedModels({String? ttsVoiceID, bool ensureVAD}); // [DIVERGE] Swift initializeVoiceAgentWithLoadedModels +void cleanup(); // [DIVERGE] Swift cleanupVoiceAgent() +Future processVoiceTurn(Uint8List audioData); // [NO-FLAT] +Stream eventStream(); // [DIVERGE] flat RunAnywhere.streamVoiceAgent() exists +// FLUTTER-ONLY: isReady, isAgentReady + +// ===================================================================== +// 12. MODELS — LIFECYCLE — FULL PARITY (flat statics) +// ===================================================================== +Future loadModel(ModelLoadRequest request); +Future unloadModel(ModelUnloadRequest request); +Future currentModel([CurrentModelRequest? request]); +Future modelInfoForCategory(ModelCategory category); +ComponentLifecycleSnapshot? componentLifecycleSnapshot(SDKComponent component); + +// ===================================================================== +// 13. MODELS — REGISTRY (RunAnywhere.models) +// ===================================================================== +Future list({ModelQuery? query}); // [DIVERGE] Swift listModels; [NO-FLAT] RunAnywhere.listModels +Future queryModels(ModelQuery query); +Future getModel(ModelGetRequest request); +Future downloadedModels(); +Future refreshModelRegistry({bool rescanLocal, bool includeRemoteCatalog, bool pruneOrphans}); // flat alias exists +ModelFileRole inferModelFileRole({required String filename, required ModelCategory modality}); +// FLUTTER-ONLY: available(), listDownloaded(), register/registerArchiveModel/registerMultiFile (on models), updateDownloadStatus, remove, currentLoadedId, resolveModelFilePath + +// ===================================================================== +// 14. DOWNLOAD +// ===================================================================== +Future downloadModel(String modelId, {Future Function(DownloadProgress)? onProgress}); +Stream start(String modelId); // [DIVERGE] Swift downloadModelStream; [NO-FLAT] +// FLUTTER-ONLY: plan, startDownload, cancelDownload, cancel, resume, pollProgress, deleteAllModels, list() + +// ===================================================================== +// 15. STORAGE — FULL PARITY (register split into 3 named methods) +// ===================================================================== +Future registerModel({String? id, required String name, required String url, required InferenceFramework framework, ModelCategory modality, ModelArtifactType? artifactType, int? memoryRequirement, bool supportsThinking, bool supportsLora}); +Future registerArchiveModel({required String archiveUrl, required ArchiveStructure structure, /* ... */}); // [DIVERGE] Swift registerModel(archive:) overload +Future registerMultiFileModel({required List files, required String id, required String name, /* ... */}); // [DIVERGE] Swift registerModel(multiFile:) overload +Future importModel(ModelImportRequest request); +Future getStorageInfo(); // [DIVERGE] on RunAnywhere.downloads +Future deleteStorage(StorageDeleteRequest request); +Future deleteModel(String modelId); +Future clearCache(); // on RunAnywhere.downloads +Future cleanTempFiles(); + +// ===================================================================== +// 16. EVENTS + MISC — FULL PARITY (different host classes) +// ===================================================================== +// EventBus (RunAnywhere.events): initializationEvents/generationEvents/modelEvents/ragEvents/ +// llmEvents/sttEvents/ttsEvents/errorEvents/sdkEvents/allEvents, voiceEventPayloads/ +// downloadEventPayloads/componentLifecycleEventPayloads/modelRegistryEventPayloads, +// modelLifecycle/modelLoaded/modelUnloaded, onCategory, publish +StreamSubscription subscribeSDKEvents(void Function(SDKEvent) handler); // [DIVERGE] returns StreamSubscription (Swift returns UInt64 token) +Future unsubscribeSDKEvents(StreamSubscription sub); +Future publishSDKEvent(SDKEvent event); +Future pollSDKEvent(); +Future publishSDKFailure({required int errorCode, required String message, required String component, required String operation, bool recoverable}); +// Logging on class RunAnywhereLogging: configureLogging/setLocalLoggingEnabled/setLogLevel/addLogDestination/setDebugMode/flushLogs (+ removeLogDestination FLUTTER-ONLY) [DIVERGE host class] +// Audio on class RunAnywhereAudioConvert: pcm16ToFloat32/pcm16ToFloat32Samples/pcm16ToWav [DIVERGE host class] +// Solutions (RunAnywhere.solutions): run({config, configBytes, yaml}) -> SolutionHandle (start/stop/cancel/closeInput/feed/destroy/isAlive) +// PluginLoader (RunAnywhere.pluginLoader): apiVersion/registeredCount/registeredNames/listLoaded/load/unload + +// ===================================================================== +// EXTRAS beyond the 16 areas (Flutter-only, not in Swift baseline) +// ===================================================================== +// - RunAnywhere.hybrid (HybridSttRouter, CloudBackend, HybridRoutingPolicy) — mirrors Swift HybridSTTRouter (not enumerated) +// - Backend packages: LlamaCpp / Onnx / QHexRT register()/unregister()/isAvailable/autoRegister/dispose + +// ===================================================================== +// CONSOLIDATED +// ===================================================================== +// TRULY MISSING (real feature gaps): webSearchToolDefinition, registerWebSearchTool (2) +// NO-FLAT (behavior present on capability object, flat RunAnywhere.* static absent): +// VAD (3), VLM (3), tts.synthesizeStream/stopSpeaking, models.list(listModels), +// downloads.start(downloadModelStream), voice.* — cosmetic surface parity only +// FLUTTER-ONLY: many convenience methods (chat, per-capability isLoaded/load/unload, +// RAG ergonomic aliases, download plan/cancel/resume, hybrid STT router) diff --git a/sdk/runanywhere-commons/docs/PublicApiKotlin.kt b/sdk/runanywhere-commons/docs/PublicApiKotlin.kt new file mode 100644 index 0000000000..5615f87b19 --- /dev/null +++ b/sdk/runanywhere-commons/docs/PublicApiKotlin.kt @@ -0,0 +1,229 @@ +/* + * PUBLIC API SNAPSHOT — Kotlin/Android SDK (sdk/runanywhere-kotlin) + * Audit reference only. NOT compilable. Signatures mirror the real facade. + * Compare 1:1 with PublicApiSwift.swift (same feature order). + * + * Markers: + * [DIVERGE] signature/shape differs from Swift + * [KT-ONLY] exists in Kotlin, absent from Swift facade (source-of-truth violation) + * [MISSING] Swift has it, Kotlin does not (see the Swift file for the entry) + */ +@file:Suppress("unused") + +// ===================================================================== +// 1. INIT / LIFECYCLE — object RunAnywhere (RunAnywhere.kt) +// ===================================================================== +val isInitialized: Boolean +val areServicesReady: Boolean +val isActive: Boolean +val version: String +val environment: SDKEnvironment? +val events: EventBus +val isAuthenticated: Boolean +val deviceId: String // [DIVERGE] Swift: throwing getter `deviceId { get throws }` +fun getUserId(): String? +fun getOrganizationId(): String? +fun isDeviceRegistered(): Boolean +fun setHfToken(token: String?) +fun initialize(apiKey: String? = null, baseURL: String? = null, environment: SDKEnvironment = SDK_ENVIRONMENT_DEVELOPMENT) +fun initialize(apiKey: String, baseURL: URL, environment: SDKEnvironment = SDK_ENVIRONMENT_PRODUCTION) +fun initialize(context: Context, apiKey: String? = null, baseURL: String? = null, environment: SDKEnvironment = SDK_ENVIRONMENT_DEVELOPMENT) // [DIVERGE] Android Context overloads (no Swift equiv, expected) +fun initialize(context: Context, apiKey: String, baseURL: URL, environment: SDKEnvironment = SDK_ENVIRONMENT_PRODUCTION) // [DIVERGE] " +suspend fun completeServicesInitialization() +suspend fun reset() + +// ===================================================================== +// 2. LLM +// ===================================================================== +suspend fun RunAnywhere.generate(prompt: String, options: RALLMGenerationOptions? = null): RALLMGenerationResult +suspend fun RunAnywhere.generate(request: RALLMGenerateRequest): RALLMGenerationResult +fun RunAnywhere.generateStream(prompt: String, options: RALLMGenerationOptions? = null): Flow +fun RunAnywhere.generateStream(request: RALLMGenerateRequest): Flow +suspend fun RunAnywhere.cancelGeneration() +suspend fun RunAnywhere.aggregateStream(prompt: String, events: Flow, onThinking: (suspend (String) -> Unit)? = null, onToken: (suspend (String) -> Unit)? = null): RALLMGenerationResult +// [MISSING] webSearchToolDefinition : RAToolDefinition +// [MISSING] suspend fun RunAnywhere.registerWebSearchTool() + +// --- LLM: Structured Output --- +suspend fun RunAnywhere.generateStructured(prompt: String, schema: RAJSONSchema, options: RALLMGenerationOptions? = null): RAStructuredOutputResult +suspend fun RunAnywhere.generateWithStructuredOutput(prompt: String, structuredOutput: StructuredOutputOptions, options: RALLMGenerationOptions? = null): RALLMGenerationResult +suspend fun RunAnywhere.extractStructuredOutput(text: String, schema: RAJSONSchema): RAStructuredOutputResult // [DIVERGE] Swift is sync `throws`, not suspend +fun RunAnywhere.generateStructuredStream(prompt: String, schema: RAJSONSchema, options: RALLMGenerationOptions? = null): Flow + +// --- LLM: Tool Calling --- +suspend fun RunAnywhere.registerTool(definition: ToolDefinition, executor: ToolExecutor) +suspend fun RunAnywhere.unregisterTool(toolName: String) +suspend fun RunAnywhere.getRegisteredTools(): List +suspend fun RunAnywhere.clearTools() +suspend fun RunAnywhere.executeTool(toolCall: ToolCall): ToolResult +suspend fun RunAnywhere.generateWithTools(prompt: String, options: RALLMGenerationOptions?, toolOptions: RAToolCallingOptions?, toolChoice: ToolChoiceMode?, forcedToolName: String?, validateCalls: Boolean? = null, history: List = emptyList()): RAToolCallingResult + +// ===================================================================== +// 3. STT +// ===================================================================== +suspend fun RunAnywhere.transcribe(audio: ByteArray, options: RASTTOptions = RASTTOptions.defaults()): RASTTOutput +fun RunAnywhere.transcribeStream(audio: Flow, options: RASTTOptions = RASTTOptions.defaults()): Flow + +// ===================================================================== +// 4. TTS +// ===================================================================== +suspend fun RunAnywhere.synthesize(text: String, options: RATTSOptions = RATTSOptions.defaults()): RATTSOutput +fun RunAnywhere.synthesizeStream(text: String, options: RATTSOptions = RATTSOptions.defaults()): Flow +suspend fun RunAnywhere.stopSynthesis() +suspend fun RunAnywhere.speak(text: String, options: RATTSOptions = RATTSOptions.defaults()): TTSSpeakResult +suspend fun RunAnywhere.stopSpeaking() + +// ===================================================================== +// 5. VAD +// ===================================================================== +suspend fun RunAnywhere.detectVoiceActivity(audioData: ByteArray, options: RAVADOptions? = null): RAVADResult +fun RunAnywhere.streamVAD(audio: Flow, options: RAVADOptions? = null): Flow +suspend fun RunAnywhere.resetVAD() + +// ===================================================================== +// 6. VLM +// ===================================================================== +suspend fun RunAnywhere.processImage(image: RAVLMImage, options: RAVLMGenerationOptions): RAVLMResult +fun RunAnywhere.processImageStream(image: RAVLMImage, options: RAVLMGenerationOptions): Flow +// [MISSING] fun RunAnywhere.processImageStream(image: RAVLMImage, prompt: String, options: RAVLMGenerationOptions = ...): Flow +suspend fun RunAnywhere.cancelVLMGeneration() + +// ===================================================================== +// 7. DIFFUSION +// ===================================================================== +suspend fun RunAnywhere.generateImage(options: RADiffusionGenerationOptions, modelId: String? = null): RADiffusionResult // [DIVERGE] Swift takes options only, no modelId +suspend fun RunAnywhere.inpaint(inputImage: ByteArray, maskImage: ByteArray, prompt: String = "Remove the masked region.", width: Int = 512, height: Int = 512, modelId: String? = null): RADiffusionResult // [KT-ONLY] +// [MISSING] fun RunAnywhere.generateImageStream(options): Flow +// [MISSING] suspend fun RunAnywhere.cancelImageGeneration() + +// ===================================================================== +// 8. EMBEDDINGS (val RunAnywhere.embeddings: Embeddings) +// ===================================================================== +suspend fun Embeddings.isLoaded(): Boolean +suspend fun Embeddings.currentModelID(): String? +suspend fun Embeddings.embed(text: String, modelId: String, options: EmbeddingsOptions? = null): RAEmbeddingsResult +suspend fun Embeddings.embedBatch(request: EmbeddingsRequest, modelId: String): RAEmbeddingsResult +suspend fun Embeddings.unload() + +// ===================================================================== +// 9. RAG +// ===================================================================== +suspend fun RunAnywhere.ragResolvedConfiguration(embeddingModel: RAModelInfo, llmModel: RAModelInfo, baseConfiguration: RARAGConfiguration): RARAGConfiguration +suspend fun RunAnywhere.ragCreatePipeline(embeddingModel: RAModelInfo, llmModel: RAModelInfo, baseConfiguration: RARAGConfiguration = RARAGConfiguration.defaults()) +suspend fun RunAnywhere.ragCreatePipeline(config: RARAGConfiguration) +suspend fun RunAnywhere.ragDestroyPipeline() +suspend fun RunAnywhere.ragIngest(document: RARAGDocument): RARAGStatistics +suspend fun RunAnywhere.ragClearDocuments() +suspend fun RunAnywhere.ragGetDocumentCount(): Int +suspend fun RunAnywhere.ragDocumentCount(): Int // [DIVERGE] Swift: async computed `var ragDocumentCount` +suspend fun RunAnywhere.ragQuery(question: String, options: RAGQueryOptions? = null): RAGResult +suspend fun RunAnywhere.ragQuery(options: RAGQueryOptions): RAGResult +fun RunAnywhere.ragQueryStream(question: String, options: RAGQueryOptions? = null): Flow +fun RunAnywhere.ragQueryStream(options: RAGQueryOptions): Flow +suspend fun RunAnywhere.ragCancelQuery() // [KT-ONLY] +suspend fun RunAnywhere.ragAddDocumentsBatch(documents: List) +suspend fun RunAnywhere.ragGetStatistics(): RARAGStatistics + +// ===================================================================== +// 10. LoRA (val RunAnywhere.lora: LoRA) +// ===================================================================== +suspend fun LoRA.apply(request: RALoRAApplyRequest): LoRAApplyResult +suspend fun LoRA.apply(entry: LoraAdapterCatalogEntry, localPath: String? = null, scale: Float? = null, replaceExisting: Boolean = false): LoRAApplyResult +// [MISSING] applyCatalogAdapter(entry, localPath, scale, replaceExisting) (KT's apply(entry,...) is behaviorally equal) +suspend fun LoRA.remove(request: RALoRARemoveRequest): RALoRAState +suspend fun LoRA.list(): RALoRAState +suspend fun LoRA.state(): RALoRAState +suspend fun LoRA.checkCompatibility(config: RALoRAAdapterConfig): LoraCompatibilityResult +suspend fun LoRA.register(entry: LoraAdapterCatalogEntry): LoraAdapterCatalogEntry +suspend fun LoRA.registerArtifact(entry: LoraAdapterCatalogEntry): RAModelInfo +suspend fun LoRA.download(entry: LoraAdapterCatalogEntry, onProgress: (suspend (DownloadProgress) -> Unit)? = null): String +suspend fun LoRA.listCatalog(request: LoraAdapterCatalogListRequest = LoraAdapterCatalogListRequest()): LoraAdapterCatalogListResult +suspend fun LoRA.queryCatalog(query: LoraAdapterCatalogQuery): LoraAdapterCatalogListResult +suspend fun LoRA.getCatalogEntry(request: LoraAdapterCatalogGetRequest): LoraAdapterCatalogGetResult +suspend fun LoRA.markDownloadCompleted(request: LoraAdapterDownloadCompletedRequest): LoraAdapterDownloadCompletedResult +suspend fun LoRA.importAdapter(sourcePath: String): LoraAdapterImportResult // [DIVERGE] Swift takes `from url: URL` +suspend fun LoRA.markImportCompleted(request: LoraAdapterDownloadCompletedRequest): LoraAdapterDownloadCompletedResult +suspend fun LoRA.adaptersForModel(modelId: String): List +suspend fun LoRA.allRegistered(): List + +// ===================================================================== +// 11. VOICE AGENT +// ===================================================================== +val RunAnywhere.defaultVADModelID: String +suspend fun RunAnywhere.ensureDefaultVAD(modelID: String? = null): Boolean +suspend fun RunAnywhere.initializeVoiceAgent(config: RAVoiceAgentComposeConfig) +suspend fun RunAnywhere.getVoiceAgentComponentStates(): RAVoiceAgentComponentStates +suspend fun RunAnywhere.initializeVoiceAgentWithLoadedModels(ttsVoiceId: String? = null, ensureVAD: Boolean = true) +suspend fun RunAnywhere.cleanupVoiceAgent() +suspend fun RunAnywhere.processVoiceTurn(audioData: ByteArray): VoiceAgentResult +fun RunAnywhere.streamVoiceAgent(): Flow + +// ===================================================================== +// 12. MODELS — LIFECYCLE +// ===================================================================== +suspend fun RunAnywhere.loadModel(request: RAModelLoadRequest): RAModelLoadResult +suspend fun RunAnywhere.loadModel(model: RAModelInfo): RAModelLoadResult // [DIVERGE] KT-extra convenience overload +suspend fun RunAnywhere.unloadModel(request: ModelUnloadRequest): ModelUnloadResult +suspend fun RunAnywhere.currentModel(request: CurrentModelRequest = CurrentModelRequest()): CurrentModelResult // [DIVERGE] Swift: SYNC, single overload +suspend fun RunAnywhere.currentModel(model: RAModelInfo): CurrentModelResult // [DIVERGE] KT-extra overload +suspend fun RunAnywhere.currentModel(candidates: Iterable): CurrentModelResult? // [DIVERGE] KT-extra overload +suspend fun RunAnywhere.modelInfoForCategory(category: ModelCategory): ModelInfo? // [DIVERGE] Swift: sync +suspend fun RunAnywhere.componentLifecycleSnapshot(component: SDKComponent): ComponentLifecycleSnapshot? + +// ===================================================================== +// 12b. MODELS — REGISTRY +// ===================================================================== +suspend fun RunAnywhere.listModels(request: ModelListRequest = ModelListRequest()): ModelListResult +suspend fun RunAnywhere.queryModels(query: ModelQuery): ModelListResult +suspend fun RunAnywhere.getModel(request: ModelGetRequest): ModelGetResult +suspend fun RunAnywhere.downloadedModels(): ModelListResult +suspend fun RunAnywhere.refreshModelRegistry(rescanLocal: Boolean = true, includeRemoteCatalog: Boolean = false, pruneOrphans: Boolean = false) +fun RunAnywhere.inferModelFileRole(filename: String, modality: ModelCategory): ModelFileRole + +// ===================================================================== +// 13. DOWNLOAD +// ===================================================================== +suspend fun RunAnywhere.downloadModel(model: RAModelInfo, onProgress: (suspend (DownloadProgress) -> Unit)? = null): DownloadProgress +fun RunAnywhere.downloadModelStream(model: RAModelInfo): Flow + +// ===================================================================== +// 14. STORAGE +// ===================================================================== +suspend fun RunAnywhere.registerModel(id: String? = null, name: String, url: String, framework: InferenceFramework, modality: ModelCategory = MODEL_CATEGORY_LANGUAGE, artifactType: ModelArtifactType? = null, memoryRequirement: Long? = null, supportsThinking: Boolean = false, supportsLora: Boolean = false): RAModelInfo +suspend fun RunAnywhere.registerModel(archiveUrl: String, structure: ArchiveStructure, id: String? = null, name: String, framework: InferenceFramework, modality: ModelCategory = MODEL_CATEGORY_LANGUAGE, archiveType: ArchiveType? = null, memoryRequirement: Long? = null, supportsThinking: Boolean = false, supportsLora: Boolean = false): RAModelInfo +suspend fun RunAnywhere.registerModel(multiFile: List, id: String, name: String, framework: InferenceFramework, modality: ModelCategory = MODEL_CATEGORY_LANGUAGE, memoryRequirement: Long? = null, contextLength: Int? = null, supportsThinking: Boolean = false, source: ModelSource = MODEL_SOURCE_REMOTE): RAModelInfo +suspend fun RunAnywhere.importModel(request: ModelImportRequest): ModelImportResult +suspend fun RunAnywhere.getStorageInfo(request: StorageInfoRequest = StorageInfoRequest()): StorageInfoResult +suspend fun RunAnywhere.deleteStorage(request: StorageDeleteRequest): StorageDeleteResult +suspend fun RunAnywhere.deleteModel(modelId: String): StorageDeleteResult +suspend fun RunAnywhere.clearCache() +suspend fun RunAnywhere.cleanTempFiles() + +// ===================================================================== +// 15. EVENTS +// ===================================================================== +// object EventBus : Flow events, start(), stop(), publish(event), +// events(category), on(scope, handler), on(scope, category, handler), +// voiceEventPayloads / downloadEventPayloads / componentLifecycleEventPayloads / modelRegistryEventPayloads, +// llmEvents / sttEvents / ttsEvents / modelEvents / errorEvents / sdkEvents / ragEvents, +// modelLifecycle / modelLoaded / modelUnloaded (EventBus+ModelLifecycle) +fun RunAnywhere.subscribeSDKEvents(handler: (SDKEvent) -> Unit): Long +fun RunAnywhere.unsubscribeSDKEvents(subscriptionId: Long) +fun RunAnywhere.publishSDKEvent(event: SDKEvent): Boolean +fun RunAnywhere.pollSDKEvent(): SDKEvent? +fun RunAnywhere.publishSDKFailure(errorCode: Int, message: String, component: String, operation: String, recoverable: Boolean = false): Boolean + +// ===================================================================== +// 16. MISC — logging / audio / solutions / plugin loader +// ===================================================================== +fun RunAnywhere.configureLogging(config: LoggingConfiguration) +fun RunAnywhere.setLocalLoggingEnabled(enabled: Boolean) +fun RunAnywhere.setLogLevel(level: LogLevel) +fun RunAnywhere.addLogDestination(destination: LogDestination) +fun RunAnywhere.setDebugMode(enabled: Boolean) +fun RunAnywhere.flushLogs() +fun RunAnywhere.pcm16ToFloat32(int16Bytes: ByteArray): ByteArray +fun RunAnywhere.pcm16ToFloat32Samples(int16Bytes: ByteArray): FloatArray +fun RunAnywhere.pcm16ToWav(int16Bytes: ByteArray, sampleRate: Int): ByteArray +// val RunAnywhere.solutions: Solutions -> run(yaml) / run(configBytes) / run(config) -> SolutionHandle +// val RunAnywhere.pluginLoader: PluginLoaderNamespace -> apiVersion, registeredCount, load, unload, registeredNames, listLoaded diff --git a/sdk/runanywhere-commons/docs/PublicApiReactNative.ts b/sdk/runanywhere-commons/docs/PublicApiReactNative.ts new file mode 100644 index 0000000000..1f330d9a1c --- /dev/null +++ b/sdk/runanywhere-commons/docs/PublicApiReactNative.ts @@ -0,0 +1,224 @@ +/* + * PUBLIC API SNAPSHOT — React Native SDK (sdk/runanywhere-react-native/packages/core) + * Audit reference only. NOT compilable. Compare against PublicApiSwift.swift (same 16 areas). + * Entry: `RunAnywhere` object (Public/RunAnywhere.ts) + Public/Extensions/*. NitroModules/JSI. + * Hermes caveat: all streams are AsyncIterable consumed via manual next()/return() — never `for await`. + * + * Markers: + * [MISSING] Swift has it, RN has NO equivalent (real gap) + * [RN-ONLY] RN has it, Swift does not + * [DIVERGE] same feature, different name/shape (RN async/Promise over the Nitro bridge) + * + * HEADLINE: RN is close to full parity. Real gaps: web-search tool (2), VLM prompt-overload, + * LoRA 2nd apply overload, standalone unsubscribeSDKEvents. Everything else is Promise-shaping. + */ + +// ===================================================================== +// 1. INIT / LIFECYCLE (RunAnywhere object) +// ===================================================================== +get isInitialized(): boolean; +get areServicesReady(): boolean; +get isActive(): boolean; +get environment(): SDKEnvironment | null; +get version(): string; +events: EventBus; +isAuthenticated(): Promise; // [DIVERGE] Swift sync Bool +getUserId(): Promise; // [DIVERGE] Swift sync String? +getOrganizationId(): Promise; // [DIVERGE] Swift sync String? +isDeviceRegistered(): Promise; // [DIVERGE] Swift sync Bool +getDeviceId(): Promise; // [RN-ONLY] method form +get deviceId(): Promise; // [DIVERGE] Swift throwing sync property +setHfToken(token: string): Promise; +initialize(options: SDKInitOptions): Promise; // [DIVERGE] single options-bag vs Swift 2 positional overloads +completeServicesInitialization(): Promise; +reset(): Promise; + +// ===================================================================== +// 2. LLM +// ===================================================================== +generate(prompt: string, options?: LLMGenerationOptions): Promise; +generate(request: LLMGenerateRequest): Promise; +generateStream(prompt: string, options?: LLMGenerationOptions): AsyncIterable; +generateStream(request: LLMGenerateRequest): AsyncIterable; +cancelGeneration(): Promise; +aggregateStream(prompt: string, iterable: AsyncIterable, onToken?: (t: string) => void | Promise): Promise; + +// --- Structured output --- +generateStructured(prompt: string, schema: JSONSchema, options?: StructuredOutputOptions): Promise; +generateWithStructuredOutput(prompt: string, structuredOutput: StructuredOutputOptions, options?: LLMGenerationOptions): Promise; +generateStructuredStream(prompt: string, schema: JSONSchema, options?: StructuredOutputOptions): AsyncIterable; +extractStructuredOutput(text: string, schema: JSONSchema): Promise; + +// --- Tools --- +registerTool(definition: ToolDefinition, executor: ToolExecutor): Promise; +unregisterTool(toolName: string): Promise; +getRegisteredTools(): Promise; +clearTools(): Promise; +executeTool(toolCall: ToolCall): Promise; +generateWithTools(prompt: string, options?: Partial, extra?: GenerateWithToolsOptions): Promise; // [RN-ONLY] extra: { signal(AbortSignal), llmOptions, validateCalls, history } + +// --- Web search --- +// [MISSING] webSearchToolDefinition +// [MISSING] registerWebSearchTool() + +// ===================================================================== +// 3. STT — FULL PARITY +// ===================================================================== +transcribe(audio: Uint8Array, options?: Partial): Promise; +transcribeStream(audio: AsyncIterable, options?: Partial): AsyncIterable; + +// ===================================================================== +// 4. TTS — FULL PARITY +// ===================================================================== +synthesize(text: string, options?: Partial): Promise; +synthesizeStream(text: string, options?: Partial): AsyncIterable; +stopSynthesis(): Promise; +speak(text: string, options?: Partial): Promise; +stopSpeaking(): Promise; + +// ===================================================================== +// 5. VAD — FULL PARITY +// ===================================================================== +detectVoiceActivity(audio: Uint8Array | Float32Array | string | ArrayBuffer, options?: Partial): Promise; // [DIVERGE] wide input union vs Swift Data +streamVAD(audio: AsyncIterable, options?: Partial): AsyncIterable; +resetVAD(): Promise; + +// ===================================================================== +// 6. VLM +// ===================================================================== +processImage(image: VLMImage, options: Partial): Promise; +processImageStream(image: VLMImage, options: Partial): Promise>; // [DIVERGE] Promise-wrapped iterable +// [MISSING] processImageStream(image, prompt, options) second overload — prompt must go in options.prompt +cancelVLMGeneration(): Promise; + +// ===================================================================== +// 7. DIFFUSION (Apple-gated; throws off-Apple) — FULL PARITY +// ===================================================================== +generateImage(options: Partial): Promise; +generateImageStream(options: Partial): Promise>; // [DIVERGE] Promise-wrapped +cancelImageGeneration(): Promise; + +// ===================================================================== +// 8. EMBEDDINGS (RunAnywhere.embeddings) — FULL PARITY +// ===================================================================== +get isLoaded(): boolean; // [DIVERGE] TS-cached vs Swift sync snapshot +get currentModelID(): string | null; +embed(text: string, modelID: string, options?: EmbeddingsOptions): Promise; +embedBatch(request: EmbeddingsRequest, modelID: string): Promise; +unload(): Promise; + +// ===================================================================== +// 9. RAG — FULL PARITY (ragCancelQuery correctly absent, matching Swift) +// ===================================================================== +ragResolvedConfiguration(embeddingModel: ModelInfo, llmModel: ModelInfo, baseConfiguration?: RAGConfiguration): Promise; +ragCreatePipeline(config: RAGConfiguration): Promise; +ragCreatePipeline(args: { embeddingModel: ModelInfo; llmModel: ModelInfo; baseConfiguration?: RAGConfiguration }): Promise; +ragDestroyPipeline(): Promise; +ragIngest(document: RAGDocument): Promise; +ragAddDocumentsBatch(documents: RAGDocument[]): Promise; +ragQuery(question: string, options?: Partial>): Promise; +ragQuery(options: RAGQueryOptions): Promise; +ragQueryStream(question: string, options?): AsyncIterable; +ragQueryStream(options: RAGQueryOptions): AsyncIterable; +ragClearDocuments(): Promise; +ragGetDocumentCount(): Promise; +ragDocumentCount(): Promise; +ragGetStatistics(): Promise; + +// ===================================================================== +// 10. LoRA (RunAnywhere.lora) +// ===================================================================== +apply(request: LoRAApplyRequest): Promise; +// [MISSING] second apply(entry, localPath?, scale?, replaceExisting?) overload — covered by applyCatalogAdapter +applyCatalogAdapter(entry: LoraAdapterCatalogEntry, options?: { localPath?: string; scale?: number; replaceExisting?: boolean }): Promise; +remove(request: LoRARemoveRequest): Promise; +list(request?: LoRAState): Promise; +state(request?: LoRAState): Promise; +checkCompatibility(config: LoRAAdapterConfig): Promise; +register(entry: LoraAdapterCatalogEntry): Promise; +registerArtifact(entry: LoraAdapterCatalogEntry): Promise; +download(entry: LoraAdapterCatalogEntry, onProgress?: (p: DownloadProgress) => void): Promise; +listCatalog(request?: LoraAdapterCatalogListRequest): Promise; +queryCatalog(query: LoraAdapterCatalogQuery): Promise; +getCatalogEntry(request: LoraAdapterCatalogGetRequest): Promise; +markDownloadCompleted(request: LoraAdapterDownloadCompletedRequest): Promise; +markImportCompleted(request: LoraAdapterDownloadCompletedRequest): Promise; +importAdapter(sourcePath: string): Promise; +adaptersForModel(modelId: string): Promise; +allRegistered(): Promise; + +// ===================================================================== +// 11. VOICE AGENT — FULL PARITY +// ===================================================================== +defaultVADModelID: string; +ensureDefaultVAD(modelID?: string): Promise; +initializeVoiceAgent(config: VoiceAgentComposeConfig): Promise; +getVoiceAgentComponentStates(): Promise; +initializeVoiceAgentWithLoadedModels(ttsVoiceID?: string, ensureVAD?: boolean): Promise; +cleanupVoiceAgent(): Promise; +processVoiceTurn(audioData: ArrayBuffer | Uint8Array): Promise; +streamVoiceAgent(): AsyncIterable; + +// ===================================================================== +// 12. MODELS — LIFECYCLE — FULL PARITY +// ===================================================================== +loadModel(request: ModelLoadRequest): Promise; +unloadModel(request: ModelUnloadRequest): Promise; +currentModel(request?: CurrentModelRequest): Promise; +modelInfoForCategory(category: ModelCategory): Promise; +componentLifecycleSnapshot(component: SDKComponent): Promise; + +// ===================================================================== +// 13. MODELS — REGISTRY — FULL PARITY +// ===================================================================== +listModels(request?: ModelListRequest): Promise; +queryModels(query: ModelQuery): Promise; +getModel(request: ModelGetRequest): Promise; +downloadedModels(): Promise; +refreshModelRegistry(options?: { rescanLocal?: boolean; includeRemoteCatalog?: boolean; pruneOrphans?: boolean }): Promise; +inferModelFileRole(filename: string, modality: ModelCategory): ModelFileRole; +getDefaultFramework(category: ModelCategory): InferenceFramework; // [RN-ONLY] Swift exposes as RAModelCategory.defaultFramework property + +// ===================================================================== +// 14. DOWNLOAD — FULL PARITY +// ===================================================================== +downloadModel(model: ModelInfo, onProgress?: (p: DownloadProgress) => void): Promise; +downloadModelStream(model: ModelInfo): AsyncIterable; + +// ===================================================================== +// 15. STORAGE — FULL PARITY (register split into 3 named fns w/ option bags) +// ===================================================================== +registerModel(input: RegisterModelInput): Promise; // url form +registerArchiveModel(input: RegisterArchiveModelInput): Promise; // [DIVERGE] Swift registerModel(archive:) overload +registerMultiFileModel(input: RegisterMultiFileModelInput): Promise; // [DIVERGE] Swift registerModel(multiFile:) overload +registerModelFromUrl(url: string, name: string, framework: InferenceFramework, options?): Promise; // [RN-ONLY] positional convenience +importModel(request: ModelImportRequest): Promise; +getStorageInfo(): Promise; +deleteStorage(request: StorageDeleteRequest): Promise; +deleteModel(modelId: string): Promise; +clearCache(): Promise; +cleanTempFiles(): Promise; // [DIVERGE] Swift returns void + +// ===================================================================== +// 16. EVENTS + MISC — near parity +// ===================================================================== +subscribeSDKEvents(callback: (event: SDKEvent) => void): Promise<() => Promise>; // [DIVERGE] returns unsubscribe closure +// [MISSING] standalone unsubscribeSDKEvents() — folded into the closure above +publishSDKEvent(event: SDKEvent): Promise; +pollSDKEvent(): Promise; +publishSDKFailure(options: { errorCode: number; message: string; component: string; operation: string; recoverable: boolean }): Promise; +// EventBus (RunAnywhere.events): on/eventsFor/llmEvents/.../voiceEventPayloads/modelLifecycle/modelLoaded/modelUnloaded + [RN-ONLY] free fn modelLifecycleChange(event) +// Logging (sync void): configureLogging/setLocalLoggingEnabled/setLogLevel/addLogDestination/setDebugMode/flushLogs +// Audio (sync): pcm16ToFloat32/pcm16ToFloat32Samples/pcm16ToWav +// Solutions (RunAnywhere.solutions): run(SolutionRunArgs union {config|configBytes|yaml}) -> SolutionHandle +// PluginLoader (RunAnywhere.pluginLoader): apiVersion/registeredCount/registeredNames/listLoaded/load/unload + +// ===================================================================== +// CONSOLIDATED +// ===================================================================== +// TRULY MISSING: webSearchToolDefinition, registerWebSearchTool, VLM prompt-overload, +// LoRA 2nd apply overload, standalone unsubscribeSDKEvents, 2nd initialize overload +// RN-ONLY: getDeviceId, registerModelFromUrl, getDefaultFramework, generateWithTools extra bag (AbortSignal), +// EventBus modelLifecycleChange free fn +// DIVERGE: auth reads async, Promise-wrapped VLM/Diffusion streams, cleanTempFiles->boolean, +// register split into 3 named fns, all streams manual-iterator AsyncIterable (Hermes) diff --git a/sdk/runanywhere-commons/docs/PublicApiSwift.swift b/sdk/runanywhere-commons/docs/PublicApiSwift.swift new file mode 100644 index 0000000000..3d973ce2f1 --- /dev/null +++ b/sdk/runanywhere-commons/docs/PublicApiSwift.swift @@ -0,0 +1,226 @@ +/* + * PUBLIC API SNAPSHOT — iOS/macOS Swift SDK (sdk/runanywhere-swift) + * Audit reference only. NOT compilable. Signatures mirror the real facade. + * Compare 1:1 with PublicApiKotlin.kt (same feature order). + * Facade type is `public enum RunAnywhere`; members live in `public extension` blocks. + * + * Markers: + * [DIVERGE] signature/shape differs from Kotlin + * [SWIFT-ONLY] exists in Swift, absent from Kotlin + * [KT-ONLY] Kotlin has it, Swift does not (see the Kotlin file) + */ + +// ===================================================================== +// 1. INIT / LIFECYCLE — enum RunAnywhere (Public/RunAnywhere.swift) +// ===================================================================== +static var isInitialized: Bool +static var areServicesReady: Bool +static var isActive: Bool +static var version: String +static var environment: SDKEnvironment? +static var events: EventBus +static var isAuthenticated: Bool +static var deviceId: String { get throws } // [DIVERGE] Kotlin: non-throwing `val deviceId: String` +static func getUserId() -> String? +static func getOrganizationId() -> String? +static func isDeviceRegistered() -> Bool +static func setHfToken(_ token: String?) +static func initialize(apiKey: String? = nil, baseURL: String? = nil, environment: SDKEnvironment = .development) throws +static func initialize(apiKey: String, baseURL: URL, environment: SDKEnvironment = .production) throws +// [KT-ONLY] initialize(context:...) overloads — Android needs a Context; no Swift equivalent (expected) +static func completeServicesInitialization() async throws +static func reset() async + +// ===================================================================== +// 2. LLM +// ===================================================================== +static func generate(prompt: String, options: RALLMGenerationOptions? = nil) async throws -> RALLMGenerationResult +static func generate(_ request: RALLMGenerateRequest) async throws -> RALLMGenerationResult +static func generateStream(prompt: String, options: RALLMGenerationOptions? = nil) async throws -> AsyncStream +static func generateStream(_ request: RALLMGenerateRequest) async throws -> AsyncStream +static func cancelGeneration() async +static func aggregateStream(prompt: String, events: AsyncStream, onThinking: ((String) async -> Void)? = nil, onToken: ((String) async -> Void)? = nil) async -> RALLMGenerationResult +static var webSearchToolDefinition: RAToolDefinition // [SWIFT-ONLY] +static func registerWebSearchTool() async // [SWIFT-ONLY] + +// --- LLM: Structured Output --- +static func generateStructured(prompt: String, schema: RAJSONSchema, options: RALLMGenerationOptions? = nil) async throws -> RAStructuredOutputResult +static func generateWithStructuredOutput(prompt: String, structuredOutput: RAStructuredOutputOptions, options: RALLMGenerationOptions? = nil) async throws -> RALLMGenerationResult +static func extractStructuredOutput(text: String, schema: RAJSONSchema) throws -> RAStructuredOutputResult // [DIVERGE] sync throws (Kotlin: suspend) +static func generateStructuredStream(prompt: String, schema: RAJSONSchema, options: RALLMGenerationOptions? = nil) throws -> AsyncThrowingStream + +// --- LLM: Tool Calling --- +static func registerTool(_ definition: RAToolDefinition, executor: @escaping ToolExecutor) async +static func unregisterTool(_ toolName: String) async +static func getRegisteredTools() async -> [RAToolDefinition] +static func clearTools() async +static func executeTool(_ toolCall: RAToolCall) async -> RAToolResult +static func generateWithTools(prompt: String, options: RALLMGenerationOptions = .defaults(), toolOptions: RAToolCallingOptions? = nil, toolChoice: RAToolChoiceMode? = nil, forcedToolName: String? = nil, validateCalls: Bool? = nil, history: [String] = []) async throws -> RAToolCallingResult + +// ===================================================================== +// 3. STT +// ===================================================================== +static func transcribe(audio audioData: Data, options: RASTTOptions = .defaults()) async throws -> RASTTOutput +static func transcribeStream(audio: AsyncStream, options: RASTTOptions = .defaults()) -> AsyncStream + +// ===================================================================== +// 4. TTS +// ===================================================================== +static func synthesize(_ text: String, options: RATTSOptions = .defaults()) async throws -> RATTSOutput +static func synthesizeStream(_ text: String, options: RATTSOptions = .defaults()) -> AsyncStream +static func stopSynthesis() async +static func speak(_ text: String, options: RATTSOptions = .defaults()) async throws -> RATTSSpeakResult +static func stopSpeaking() async + +// ===================================================================== +// 5. VAD +// ===================================================================== +static func detectVoiceActivity(_ audioData: Data, options: RAVADOptions? = nil) async throws -> RAVADResult +static func streamVAD(audio: AsyncStream, options: RAVADOptions? = nil) -> AsyncStream +static func resetVAD() async throws + +// ===================================================================== +// 6. VLM +// ===================================================================== +static func processImage(_ image: RAVLMImage, options: RAVLMGenerationOptions) async throws -> RAVLMResult +static func processImageStream(_ image: RAVLMImage, options: RAVLMGenerationOptions) async throws -> AsyncStream +static func processImageStream(_ image: RAVLMImage, prompt: String, options: RAVLMGenerationOptions = .defaults()) async throws -> AsyncStream // [SWIFT-ONLY] +static func cancelVLMGeneration() async + +// ===================================================================== +// 7. DIFFUSION +// ===================================================================== +static func generateImage(_ options: RADiffusionGenerationOptions) async throws -> RADiffusionResult // [DIVERGE] Kotlin adds modelId param +static func generateImageStream(_ options: RADiffusionGenerationOptions) async throws -> AsyncStream // [SWIFT-ONLY] +static func cancelImageGeneration() async // [SWIFT-ONLY] +// [KT-ONLY] inpaint(inputImage:maskImage:prompt:width:height:modelId:) — no Swift facade method + +// ===================================================================== +// 8. EMBEDDINGS (static var embeddings: Embeddings) +// ===================================================================== +var isLoaded: Bool +var currentModelID: String? +func embed(_ text: String, modelID: String, options: RAEmbeddingsOptions? = nil) async throws -> RAEmbeddingsResult +func embedBatch(_ request: RAEmbeddingsRequest, modelID: String) async throws -> RAEmbeddingsResult +func unload() async throws + +// ===================================================================== +// 9. RAG +// ===================================================================== +static func ragResolvedConfiguration(embeddingModel: RAModelInfo, llmModel: RAModelInfo, baseConfiguration: RARAGConfiguration = .defaults()) async throws -> RARAGConfiguration +static func ragCreatePipeline(embeddingModel: RAModelInfo, llmModel: RAModelInfo, baseConfiguration: RARAGConfiguration = .defaults()) async throws +static func ragCreatePipeline(config: RARAGConfiguration) async throws +static func ragDestroyPipeline() async +static func ragIngest(_ document: RARAGDocument) async throws -> RARAGStatistics +static func ragClearDocuments() async throws +static func ragGetDocumentCount() async -> Int +static var ragDocumentCount: Int { get async } // [DIVERGE] Kotlin: suspend fun ragDocumentCount() +static func ragQuery(question: String, options: RARAGQueryOptions? = nil) async throws -> RARAGResult +static func ragQuery(_ options: RARAGQueryOptions) async throws -> RARAGResult +static func ragQueryStream(question: String, options: RARAGQueryOptions? = nil) async throws -> AsyncStream +static func ragQueryStream(_ options: RARAGQueryOptions) async throws -> AsyncStream +// [KT-ONLY] ragCancelQuery() — Swift cancels via stream backpressure (break the AsyncStream) +static func ragAddDocumentsBatch(documents: [RARAGDocument]) async throws +static func ragGetStatistics() async throws -> RARAGStatistics + +// ===================================================================== +// 10. LoRA (static var lora: LoRA) +// ===================================================================== +func apply(_ request: RALoRAApplyRequest) async throws -> RALoRAApplyResult +func apply(_ entry: RALoraAdapterCatalogEntry, localPath: String? = nil, scale: Float? = nil, replaceExisting: Bool = false) async throws -> RALoRAApplyResult +func applyCatalogAdapter(_ entry: RALoraAdapterCatalogEntry, localPath: String? = nil, scale: Float? = nil, replaceExisting: Bool = false) async throws -> RALoRAApplyResult // [SWIFT-ONLY] +func remove(_ request: RALoRARemoveRequest) async throws -> RALoRAState +func list() async throws -> RALoRAState +func state() async throws -> RALoRAState +func checkCompatibility(_ config: RALoRAAdapterConfig) async -> RALoraCompatibilityResult +func register(_ entry: RALoraAdapterCatalogEntry) async throws -> RALoraAdapterCatalogEntry +func registerArtifact(_ entry: RALoraAdapterCatalogEntry) async throws -> RAModelInfo +func download(_ entry: RALoraAdapterCatalogEntry, onProgress: ((RADownloadProgress) async -> Void)? = nil) async throws -> String +func listCatalog(_ request: RALoraAdapterCatalogListRequest = RALoraAdapterCatalogListRequest()) async throws -> RALoraAdapterCatalogListResult +func queryCatalog(_ query: RALoraAdapterCatalogQuery) async throws -> RALoraAdapterCatalogListResult +func getCatalogEntry(_ request: RALoraAdapterCatalogGetRequest) async throws -> RALoraAdapterCatalogGetResult +func markDownloadCompleted(_ request: RALoraAdapterDownloadCompletedRequest) async throws -> RALoraAdapterDownloadCompletedResult +func importAdapter(from url: URL) async throws -> RALoraAdapterImportResult // [DIVERGE] Kotlin takes sourcePath: String +func markImportCompleted(_ request: RALoraAdapterDownloadCompletedRequest) async throws -> RALoraAdapterDownloadCompletedResult +func adaptersForModel(_ modelId: String) async throws -> [RALoraAdapterCatalogEntry] +func allRegistered() async throws -> [RALoraAdapterCatalogEntry] + +// ===================================================================== +// 11. VOICE AGENT +// ===================================================================== +static var defaultVADModelID: String +static func ensureDefaultVAD(modelID: String? = nil) async -> Bool +static func initializeVoiceAgent(_ config: RAVoiceAgentComposeConfig) async throws +static func getVoiceAgentComponentStates() async throws -> RAVoiceAgentComponentStates +static func initializeVoiceAgentWithLoadedModels(ttsVoiceID: String? = nil, ensureVAD: Bool = true) async throws +static func cleanupVoiceAgent() async +static func processVoiceTurn(_ audioData: Data) async throws -> RAVoiceAgentResult +static func streamVoiceAgent() -> AsyncStream + +// ===================================================================== +// 12. MODELS — LIFECYCLE +// ===================================================================== +static func loadModel(_ request: RAModelLoadRequest) async -> RAModelLoadResult // [DIVERGE] async NON-throwing +// [KT-ONLY] loadModel(model: RAModelInfo) convenience overload +static func unloadModel(_ request: RAModelUnloadRequest) async -> RAModelUnloadResult +static func currentModel(_ request: RACurrentModelRequest = RACurrentModelRequest()) -> RACurrentModelResult // [DIVERGE] SYNC, single overload (Kotlin: suspend + 3 overloads) +static func modelInfoForCategory(_ category: RAModelCategory) -> RAModelInfo? // [DIVERGE] sync (Kotlin: suspend) +static func componentLifecycleSnapshot(_ component: RASDKComponent) -> RAComponentLifecycleSnapshot? + +// ===================================================================== +// 12b. MODELS — REGISTRY +// ===================================================================== +static func listModels(_ request: RAModelListRequest = RAModelListRequest()) async -> RAModelListResult +static func queryModels(_ query: RAModelQuery) async -> RAModelListResult +static func getModel(_ request: RAModelGetRequest) async -> RAModelGetResult +static func downloadedModels() async -> RAModelListResult +static func refreshModelRegistry(rescanLocal: Bool = true, includeRemoteCatalog: Bool = false, pruneOrphans: Bool = false) async +static func inferModelFileRole(filename: String, modality: ModelCategory) -> RAModelFileRole + +// ===================================================================== +// 13. DOWNLOAD +// ===================================================================== +static func downloadModel(_ model: RAModelInfo, onProgress: ((RADownloadProgress) async -> Void)? = nil) async throws -> RADownloadProgress +static func downloadModelStream(_ model: RAModelInfo) -> AsyncThrowingStream + +// ===================================================================== +// 14. STORAGE +// ===================================================================== +static func registerModel(id: String? = nil, name: String, url: String, framework: InferenceFramework, modality: ModelCategory = .language, artifactType: RAModelArtifactType? = nil, memoryRequirement: Int64? = nil, supportsThinking: Bool = false, supportsLora: Bool = false) async throws -> RAModelInfo +static func registerModel(archive url: String, structure: RAArchiveStructure, id: String? = nil, name: String, framework: InferenceFramework, modality: ModelCategory = .language, archiveType: RAArchiveType? = nil, memoryRequirement: Int64? = nil, supportsThinking: Bool = false, supportsLora: Bool = false) async throws -> RAModelInfo +static func registerModel(multiFile descriptors: [RAModelFileDescriptor], id: String, name: String, framework: InferenceFramework, modality: ModelCategory = .language, memoryRequirement: Int64? = nil, contextLength: Int? = nil, supportsThinking: Bool = false, source: RAModelSource = .remote) async throws -> RAModelInfo +static func importModel(_ request: RAModelImportRequest) async throws -> RAModelImportResult +static func getStorageInfo(_ request: RAStorageInfoRequest = RAStorageInfoRequest()) async -> RAStorageInfoResult +static func deleteStorage(_ request: RAStorageDeleteRequest) async -> RAStorageDeleteResult +static func deleteModel(_ modelId: String) async -> RAStorageDeleteResult +static func clearCache() async throws +static func cleanTempFiles() async throws + +// ===================================================================== +// 15. EVENTS +// ===================================================================== +// final class EventBus : events (AnyPublisher), start(), stop(), publish(_), +// events(for category), on(_ handler), on(_ category:handler:), +// voiceEventPayloads / downloadEventPayloads / componentLifecycleEventPayloads / modelRegistryEventPayloads, +// llmEvents / sttEvents / ttsEvents / modelEvents / errorEvents / sdkEvents / ragEvents, +// modelLifecycle / modelLoaded / modelUnloaded (EventBus+ModelLifecycle) +static func subscribeSDKEvents(_ handler: @escaping @Sendable (RASDKEvent) -> Void) -> UInt64 +static func unsubscribeSDKEvents(_ subscriptionId: UInt64) +static func publishSDKEvent(_ event: RASDKEvent) -> Bool +static func pollSDKEvent() -> RASDKEvent? +static func publishSDKFailure(errorCode: rac_result_t, message: String, component: String, operation: String, recoverable: Bool = false) -> Bool + +// ===================================================================== +// 16. MISC — logging / audio / solutions / plugin loader +// ===================================================================== +static func configureLogging(_ config: RALoggingConfiguration) +static func setLocalLoggingEnabled(_ enabled: Bool) +static func setLogLevel(_ level: RALogLevel) +static func addLogDestination(_ destination: LogDestination) +static func setDebugMode(_ enabled: Bool) +static func flushLogs() +static func pcm16ToFloat32(_ int16Data: Data) -> Data +static func pcm16ToFloat32Samples(_ int16Data: Data) -> [Float] +static func pcm16ToWav(_ int16Data: Data, sampleRate: Int) -> Data +// static var solutions: Solutions -> run(configBytes:) / run(config:) / run(yaml:) -> SolutionHandle +// static var pluginLoader: PluginLoaderNamespace -> apiVersion, load, unload, registeredCount, registeredNames, listLoaded diff --git a/sdk/runanywhere-commons/docs/PublicApiWeb.ts b/sdk/runanywhere-commons/docs/PublicApiWeb.ts new file mode 100644 index 0000000000..e9aa12f955 --- /dev/null +++ b/sdk/runanywhere-commons/docs/PublicApiWeb.ts @@ -0,0 +1,243 @@ +/* + * PUBLIC API SNAPSHOT — Web SDK (sdk/runanywhere-web/packages/core) + * Audit reference only. NOT compilable. Compare against PublicApiSwift.swift (same 16 areas). + * Entry: `RunAnywhere` object (Public/RunAnywhere.ts) + spread ...flatFacade (RunAnywhere+FlatFacade.ts). + * Emscripten WASM + TS. Streaming is AsyncIterable; LLM stream returns a rich LLMStreamingResult. + * (Single committed source tree — no old/next split.) + * + * Markers: + * [MISSING] Swift has it, Web has NO public equivalent (real gap) + * [WEB-ONLY] Web has it, Swift does not (browser/WASM plumbing — mostly justified) + * [DIVERGE] same feature, different name/shape + * + * HEADLINE: Web has the MOST gaps AND the most extras. Real feature gaps: setHfToken, + * web-search tool, ragDocumentCount, public inferModelFileRole, standalone unsubscribeSDKEvents. + * Diffusion absent = expected (Apple-only). Huge WEB-ONLY surface for browser storage/handles/hybrid. + */ + +// ===================================================================== +// 1. INIT / LIFECYCLE (RunAnywhere object) +// ===================================================================== +get isInitialized(): boolean; +get areServicesReady(): boolean; +get isActive(): boolean; +get version(): string; +get environment(): SDKEnvironment | null; +get events(): EventBus; +get deviceId(): string; // [DIVERGE] throwing getter vs Swift deviceId() throws +get isAuthenticated(): boolean; +getUserId(): string | null; +getOrganizationId(): string | null; +isDeviceRegistered(): boolean; +// [MISSING] setHfToken — no HF-token setter anywhere in packages/*/src +initialize(options: SDKInitOptions): Promise; // [DIVERGE] single options-bag vs Swift 2 overloads +completeServicesInitialization(): Promise; +reset(): Promise; // delegates to shutdown() +// [WEB-ONLY] runtime getter, setRuntime(mode), ensureServicesReady(), hydrateModelRegistry(), +// shutdown(), storage namespace (FS Access / OPFS) + +// ===================================================================== +// 2. LLM +// ===================================================================== +generate(options: TextGenerationOptions, extra?: CancellableCall): Promise; // [DIVERGE] options-object (prompt is a field), no (prompt, options) top-level form +generateStream(options, extra?): Promise; // [DIVERGE] returns { events, stream, result, cancel } not bare iterable +cancelGeneration(): void; +// textGeneration.generate(request) / generate(options) overloads exist under RunAnywhere.textGeneration +// aggregateStream(prompt, streaming, onToken?, onThinking?) — [DIVERGE] only under RunAnywhere.textGeneration, not flat + +// --- Structured output (flat) --- +generateStructured(prompt: string, schema: StructuredOutputSchema, options?): Promise; +generateWithStructuredOutput(prompt: string, structuredOutput: Partial, options?): Promise; +generateStructuredStream(prompt: string, schema, options?): AsyncIterable; +extractStructuredOutput(text: string, schema): StructuredOutputResult; +// [WEB-ONLY] structuredOutput.{supportsProtoStructuredOutput, preparePrompt, validate} + +// --- Tools (RunAnywhere.toolCalling.*) --- +registerTool(definition: ToolDefinition, executor: ToolExecutor): void; +unregisterTool(name: string): void; +getRegisteredTools(): ToolDefinition[]; +clearTools(): void; +executeTool(toolCall: ToolCall): Promise; +generateWithTools(prompt: string, options?: Partial, extra?: GenerateWithToolsOptions): Promise; // [DIVERGE] extra: { signal, llmOptions, validateCalls, history } +// [WEB-ONLY] toolCalling.{parse, parseToolCall, formatPrompt, validateCall, buildInitialPrompt, buildFollowupPrompt, supportsProtoToolCalling, ...} + +// --- Web search --- +// [MISSING] webSearchToolDefinition +// [MISSING] registerWebSearchTool() + +// ===================================================================== +// 3. STT — FULL PARITY (+ handle namespace) +// ===================================================================== +transcribe(audio: Uint8Array | Float32Array, options?: TranscribeOptions, extra?: CancellableCall): Promise; +transcribeStream(audio, options?): AsyncIterable; +// [WEB-ONLY] stt.{create, loadModel, isLoaded, transcribe(handle,...), unload, destroy, supportsProtoSTT} + +// ===================================================================== +// 4. TTS +// ===================================================================== +synthesize(text: string, options?: SynthesizeOptions, extra?): Promise; +synthesizeStream(handle, text: string, options, extra?): AsyncIterable; // [DIVERGE] requires a component handle (no lifecycle-auto top-level form) +stopSynthesis(handle): boolean; // [DIVERGE] takes handle +speak(text: string, options?): Promise; +stopSpeaking(handle?): boolean; // [DIVERGE] optional handle +// [WEB-ONLY] tts.{create, loadVoice, listVoices, listLoadedVoices, synthesize, stop, destroy} + +// ===================================================================== +// 5. VAD +// ===================================================================== +detectVoiceActivity(audio: Float32Array, options?: DetectVoiceOptions): Promise; +streamVAD(audio: AsyncIterable, options?): AsyncIterable; // native stream requires 16kHz (throws otherwise) +resetVAD(handle): boolean; // [DIVERGE] requires handle vs Swift parameterless +// [WEB-ONLY] vad.{create, configure, initialize, loadModel, process, statistics, setActivityHandler, start, stop, reset, destroy} + +// ===================================================================== +// 6. VLM — FULL PARITY (both overloads present) +// ===================================================================== +processImage(image: VLMImage, options: VLMGenerationOptions, extra?: CancellableCall): Promise; +processImageStream(image: VLMImage, options: VLMGenerationOptions): Promise>; // [DIVERGE] Promise-wrapped +processImageStream(image: VLMImage, prompt: string, options?: VLMGenerationOptions): Promise>; +cancelVLMGeneration(): Promise; + +// ===================================================================== +// 7. DIFFUSION (Apple-only in Swift) +// ===================================================================== +// [MISSING] generateImage / generateImageStream / cancelImageGeneration — NO public facade. +// Only an internal DiffusionProtoAdapter exists. Expected non-parity (Apple/CoreML only). + +// ===================================================================== +// 8. EMBEDDINGS (RunAnywhere.embeddings) — FULL PARITY +// ===================================================================== +get isLoaded(): boolean; +get currentModelID(): string | null; +embed(text: string, modelID: string, options?: EmbeddingsOptions): Promise; // [DIVERGE] modelID required positional +embedBatch(request: EmbeddingsRequest, modelID: string): Promise; +unload(): Promise; +// [WEB-ONLY] embeddingCosineSimilarity, embeddingComputeNorm, embeddingsResultProcessingTime + +// ===================================================================== +// 9. RAG (flat verbs) +// ===================================================================== +ragResolvedConfiguration(embeddingModelId, llmModelId, baseConfiguration?): Promise; +ragCreatePipeline(config): Promise; +ragCreatePipeline(embeddingModelId, llmModelId, baseConfiguration?): Promise; +ragDestroyPipeline(): Promise; +ragIngest(text: string, metadataJson?): Promise; +ragIngest(document: RAGDocument): Promise; +ragClearDocuments(): Promise; +ragGetDocumentCount(): Promise; +// [MISSING] ragDocumentCount — only ragGetDocumentCount exists (Swift has both) +ragQuery(question: string, options?): Promise; +ragQuery(options: RAGQueryOptions): Promise; +ragQueryStream(question: string, options?): AsyncIterable; +ragQueryStream(options: RAGQueryOptions): AsyncIterable; +ragAddDocumentsBatch(documents: Array<{ text: string; metadataJson?: string }>): Promise; +ragGetStatistics(): Promise; +// ragCancelQuery correctly absent (matches Swift) +// [WEB-ONLY] rag.{setProvider, createNativeProvider, availability, pipelineState, ensureReady, listDocuments, removeDocument, capabilities} + +// ===================================================================== +// 10. LoRA (RunAnywhere.lora) +// ===================================================================== +apply(request: LoRAApplyRequest): Promise; +// [MISSING] second apply overload — covered by applyCatalogAdapter +applyCatalogAdapter(entry, options?): Promise; +remove(request): Promise; +list(request?): Promise; +state(request?): Promise; +checkCompatibility(config): Promise; +register(entry): Promise; +registerArtifact(entry): Promise; +download(entry, onProgress?): Promise; +listCatalog(request?): Promise; +queryCatalog(query): Promise; +getCatalogEntry(request): Promise; +markDownloadCompleted(request): Promise; +markImportCompleted(request): Promise; +importAdapter(file: File | Blob, filename?: string): Promise; // [DIVERGE] browser File/Blob vs Swift URL/path +adaptersForModel(modelId): Promise; +allRegistered(): Promise; +// [WEB-ONLY] lora.{supportsNative, missingExports, supportsNativeCatalog, catalog} + +// ===================================================================== +// 11. VOICE AGENT (flat verbs) — FULL PARITY +// ===================================================================== +defaultVADModelID: string; +ensureDefaultVAD(modelID?: string): Promise; +initializeVoiceAgent(config: VoiceAgentComposeConfig): Promise; +getVoiceAgentComponentStates(): Promise; +initializeVoiceAgentWithLoadedModels(ttsVoiceID?: string, ensureVAD?: boolean): Promise; +cleanupVoiceAgent(): Promise; +processVoiceTurn(audio: Float32Array | Uint8Array): Promise; +streamVoiceAgent(req?: VoiceAgentRequest, signal?: AbortSignal): AsyncIterable; // [DIVERGE] AbortSignal + default req +// [WEB-ONLY] voiceAgent.{availability, isAvailable, isReady, areAllComponentsReady, transcribe, generateResponse, synthesizeSpeech} + +// ===================================================================== +// 12. MODELS — LIFECYCLE — FULL PARITY +// ===================================================================== +loadModel(request: ModelLoadRequest): Promise; +unloadModel(request: ModelUnloadRequest): Promise; +currentModel(request?: CurrentModelRequest): CurrentModelResult | null; +modelInfoForCategory(category: ModelCategory): ModelInfo | null; +componentLifecycleSnapshot(component: SDKComponent): ComponentLifecycleSnapshot | null; + +// ===================================================================== +// 13. MODELS — REGISTRY +// ===================================================================== +listModels(): ModelInfoList | null; +queryModels(query: ModelQuery): ModelInfoList | null; +getModel(modelId: string): ModelInfo | null; // [DIVERGE] takes id vs Swift ModelGetRequest +downloadedModels(): ModelInfoList | null; +refreshModelRegistry(options?: RefreshOptions): boolean; +// [MISSING] inferModelFileRole — only internal ProtoWasmBridge.inferModelFileRole, not on public surface +getDefaultFramework(category: ModelCategory): InferenceFramework; // [WEB-ONLY] +// [WEB-ONLY] modelRegistry.{registerModel, importModel, updateModel, updateDownloadStatus, removeModel, availability, defaultFramework} + +// ===================================================================== +// 14. DOWNLOAD — FULL PARITY +// ===================================================================== +downloadModel(input: string | DownloadModelOptions, extra?: CancellableCall): Promise; // [DIVERGE] accepts id string or options w/ onProgress +downloadModelStream(input: string | DownloadModelOptions, extra?): AsyncIterable; + +// ===================================================================== +// 15. STORAGE — FULL PARITY (register split into 3 named positional fns) +// ===================================================================== +registerModel(url: string, name: string, framework: InferenceFramework, options?: RegisterModelOptions): ModelInfo; +registerModelArchive(url: string, name: string, framework: InferenceFramework, archiveType: ModelArtifactType, options?): ModelInfo; // [DIVERGE] Swift registerModel(archive:) overload +registerModelMultiFile(options: RegisterMultiFileOptions): ModelInfo; // [DIVERGE] Swift registerModel(multiFile:) overload +importModel(request: ModelImportRequest): ModelImportResult; +getStorageInfo(request): StorageInfoResult; +deleteStorage(request): Promise; +deleteModel(modelId: string): Promise; +clearCache(): Promise; +cleanTempFiles(): Promise; +// [WEB-ONLY] storage.{isLocalStorageSupported, chooseLocalStorageDirectory, restoreLocalStorage, requestLocalStorageAccess, info, availability, delete, deleteModel} (FS Access / OPFS) + +// ===================================================================== +// 16. EVENTS + MISC +// ===================================================================== +// SDK events (RunAnywhere.sdkEvents.*): +// subscribe(handler) -> SDKEventUnsubscribe | null [DIVERGE] closure, no standalone unsubscribeSDKEvents [MISSING] +// publish(event) -> boolean; poll() -> SDKEvent | null; publishFailure({...}) -> boolean; clearQueue() [WEB-ONLY] +// EventBus (RunAnywhere.events): modelLifecycle/modelLoaded/modelUnloaded/modelLifecycleChange +// Logging (flat, sync void): configureLogging/setLocalLoggingEnabled/setLogLevel/addLogDestination/setDebugMode/flushLogs +// Audio (flat, sync): pcm16ToFloat32/pcm16ToFloat32Samples/pcm16ToWav +// Solutions (RunAnywhere.solutions): run(SolutionRunInput) -> SolutionHandle +// PluginLoader (RunAnywhere.pluginLoader): apiVersion/registeredCount/registeredNames/listLoaded/load/unload + +// ===================================================================== +// EXTRAS beyond the 16 areas +// ===================================================================== +// - Hybrid STT (entire area): RunAnywhere.hybrid.*, HybridSttRouter, Cloud/cloud, +// registerCloudSttProvider/unregisterCloudSttProvider, setHybridDeviceStateProvider +// - Backend/runtime plumbing: setStreamWorkerFactory, @runanywhere/web/{backend,internal,browser} subpaths + +// ===================================================================== +// CONSOLIDATED +// ===================================================================== +// TRULY MISSING (real gaps): setHfToken, webSearchToolDefinition, registerWebSearchTool, +// ragDocumentCount, public inferModelFileRole, standalone unsubscribeSDKEvents +// EXPECTED-MISSING: diffusion (Apple-only) +// WEB-ONLY (mostly justified): runtime/storage browser namespaces, stt/tts/vad handle namespaces, +// toolCalling primitives, rag provider namespace, voiceAgent namespace, hybrid STT, modelRegistry namespace +// DIVERGE: handle-based tts/vad top-level, options-bag generate, Promise-wrapped VLM stream, +// register split into 3 named fns, sdkEvents.* naming, AbortSignal cancellation diff --git a/sdk/runanywhere-commons/docs/md/flutter.md b/sdk/runanywhere-commons/docs/md/flutter.md new file mode 100644 index 0000000000..da79ad0834 --- /dev/null +++ b/sdk/runanywhere-commons/docs/md/flutter.md @@ -0,0 +1,146 @@ +# RunAnywhere Flutter SDK — Public API Usage + +iOS + Android via Dart FFI. Entry point is `class RunAnywhere` with `static` members. Two equivalent access styles: **flat** `RunAnywhere.x(...)` (mirrors Swift) and **capability objects** `RunAnywhere.vad.x(...)`. Streaming uses Dart `Stream`; async uses `Future`. + +## Initialization + +```dart +import 'package:runanywhere/runanywhere.dart'; + +// Phase 1 +await RunAnywhere.initialize( + apiKey: 'ra_...', // optional in development + environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, +); + +// Phase 2 +await RunAnywhere.completeServicesInitialization(); + +// State +RunAnywhere.isInitialized; +RunAnywhere.areServicesReady; +RunAnywhere.version; +RunAnywhere.deviceId; // getter, throws if unresolved +RunAnywhere.isAuthenticated; +``` + +## Models + +```dart +final models = await RunAnywhere.listModels(); +final downloaded = await RunAnywhere.downloadedModels(); +final one = await RunAnywhere.getModel(ModelGetRequest(modelId: 'qwen2.5-0.5b')); + +final info = await RunAnywhere.registerModel( + name: 'Qwen2.5 0.5B', + url: 'https://huggingface.co/.../model.gguf', + framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP, +); + +await RunAnywhere.downloadModel(info.id, onProgress: (p) async => print(p.percentage)); +await RunAnywhere.loadModel(ModelLoadRequest(modelId: info.id)); + +RunAnywhere.downloadModelStream(info.id).listen((p) => print(p.state)); +``` + +## LLM — text generation + +```dart +final result = await RunAnywhere.generate('Explain vector databases in one line.'); +print(result.text); + +RunAnywhere.generateStream('Write a haiku.').listen((e) => print(e.token)); +RunAnywhere.cancelGeneration(); + +// Dart has no method overloading — the request forms are distinct names: +await RunAnywhere.generateRequest(LLMGenerateRequest(/* ... */)); +RunAnywhere.generateStreamRequest(LLMGenerateRequest(/* ... */)); +``` + +### Structured output + +```dart +final structured = await RunAnywhere.generateStructured(prompt: 'Extract', schema: schema); +final extracted = RunAnywhere.extractStructuredOutput(text: raw, schema: schema); +``` + +### Tool calling + +```dart +RunAnywhere.registerTool(toolDefinition, (args) async => {'result': ToolValues.string('ok')}); +final toolResult = await RunAnywhere.generateWithTools('Weather in Pune?'); +``` + +## STT / TTS / VAD + +```dart +final transcript = await RunAnywhere.transcribe(pcm16); +RunAnywhere.transcribeStream(audioStream).listen((p) => print(p.text)); + +final audio = await RunAnywhere.synthesize('Hello there'); +RunAnywhere.synthesizeStream('Streamed speech').listen((chunk) {}); +await RunAnywhere.speak('Spoken aloud'); +await RunAnywhere.stopSpeaking(); + +final vad = await RunAnywhere.detectVoiceActivity(pcm16); +RunAnywhere.streamVAD(audioStream).listen((r) => print(r.isSpeech)); +RunAnywhere.resetVAD(); +``` + +## VLM (vision) + +```dart +final out = await RunAnywhere.processImage(image, prompt: 'Describe this.'); +RunAnywhere.processImageStream(image, prompt: 'What is this?').listen(print); +await RunAnywhere.cancelVLMGeneration(); +``` + +## Diffusion (Apple / CoreML only) + +```dart +final image = await RunAnywhere.generateImage(diffusionOptions); +RunAnywhere.generateImageStream(diffusionOptions).listen(print); +await RunAnywhere.cancelImageGeneration(); +``` + +## RAG + +```dart +await RunAnywhere.ragCreatePipelineForModels(embeddingModel: emb, llmModel: llm); +await RunAnywhere.ragIngest(document); +final answer = await RunAnywhere.ragQuery(RAGQueryOptions(question: 'Pricing?')); +RunAnywhere.ragQueryStream(RAGQueryOptions(question: 'Summarize')).listen(print); +``` + +## LoRA + +```dart +await RunAnywhere.lora.applyCatalogAdapter(catalogEntry, scale: 1.0); +final state = await RunAnywhere.lora.list(); +await RunAnywhere.lora.download(catalogEntry, onProgress: (p) => print(p)); +``` + +## Voice agent + +```dart +await RunAnywhere.initializeVoiceAgentWithLoadedModels(); +RunAnywhere.streamVoiceAgent().listen((event) => print(event)); +final turn = await RunAnywhere.processVoiceTurn(pcm16); +RunAnywhere.cleanupVoiceAgent(); +``` + +## Events + +```dart +RunAnywhere.events.llmEvents.listen(print); +RunAnywhere.events.modelLoaded.listen((c) => print('loaded ${c.modelId}')); + +final sub = RunAnywhere.subscribeSDKEvents((event) => print(event)); +await RunAnywhere.unsubscribeSDKEvents(sub); +``` + +## Notes + +- Flat methods mirror Swift 1:1; capability objects (`RunAnywhere.vad`, `.vlm`, `.voice`, `.models`, `.lora`, ...) offer the same features plus Dart-only conveniences (`load`/`unload`/`isLoaded`, `query(question)`, etc.). +- Dart has no method overloading, so Swift's overloaded `generate`/`registerModel` map to distinct names (`generateRequest`, `registerArchiveModel`, `registerMultiFileModel`). +- Streaming uses `Stream`; cancel by cancelling the `StreamSubscription`. diff --git a/sdk/runanywhere-commons/docs/md/kotlin.md b/sdk/runanywhere-commons/docs/md/kotlin.md new file mode 100644 index 0000000000..df020b6f28 --- /dev/null +++ b/sdk/runanywhere-commons/docs/md/kotlin.md @@ -0,0 +1,155 @@ +# RunAnywhere Kotlin SDK — Public API Usage + +Android library. Entry point is the `object RunAnywhere`; every feature is a suspend/Flow extension function on it. Structured types come from generated Wire proto messages (`RA*` typealiases). + +## Initialization + +```kotlin +import com.runanywhere.sdk.public.RunAnywhere + +// Phase 1 — synchronous registration (call from Application.onCreate) +RunAnywhere.initialize( + context = applicationContext, + apiKey = "ra_...", // optional in development + environment = SDK_ENVIRONMENT_DEVELOPMENT, +) + +// Phase 2 — async services (auth, device registration, model assignments) +RunAnywhere.completeServicesInitialization() + +// State +RunAnywhere.isInitialized // Boolean +RunAnywhere.areServicesReady // Boolean +RunAnywhere.version // String +RunAnywhere.deviceId // String (throws if identity unresolved) +RunAnywhere.isAuthenticated +``` + +## Models + +```kotlin +// Discover / query +val models = RunAnywhere.listModels() +val downloaded = RunAnywhere.downloadedModels() +val one = RunAnywhere.getModel(ModelGetRequest(model_id = "qwen2.5-0.5b")) + +// Register a remote model +val info = RunAnywhere.registerModel( + name = "Qwen2.5 0.5B", + url = "https://huggingface.co/.../model.gguf", + framework = InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP, +) + +// Download (with progress) then load +RunAnywhere.downloadModel(info) { progress -> println("${progress.percentage}%") } +RunAnywhere.loadModel(info) + +// Streaming download +RunAnywhere.downloadModelStream(info).collect { println(it.state) } +``` + +## LLM — text generation + +```kotlin +// One-shot +val result = RunAnywhere.generate("Explain vector databases in one line.") +println(result.text) + +// Streaming +RunAnywhere.generateStream("Write a haiku about the sea.").collect { event -> + event.token?.let { print(it) } +} + +RunAnywhere.cancelGeneration() +``` + +### Structured output + +```kotlin +val schema: RAJSONSchema = /* build JSON schema */ +val structured = RunAnywhere.generateStructured("Extract name + age", schema) +``` + +### Tool calling + +```kotlin +RunAnywhere.registerTool(toolDefinition) { args -> mapOf("result" to ToolValue.string("ok")) } +val toolResult = RunAnywhere.generateWithTools( + prompt = "What's the weather in Pune?", + options = null, toolOptions = null, toolChoice = null, forcedToolName = null, +) + +// Built-in web search tool (DuckDuckGo, over OkHttp) +RunAnywhere.registerWebSearchTool() +val definition = RunAnywhere.webSearchToolDefinition +``` + +## STT / TTS / VAD + +```kotlin +val transcript = RunAnywhere.transcribe(pcm16Bytes) +RunAnywhere.transcribeStream(audioFlow).collect { println(it.text) } + +val audio = RunAnywhere.synthesize("Hello there") +RunAnywhere.speak("Spoken aloud") +RunAnywhere.stopSpeaking() + +val vad = RunAnywhere.detectVoiceActivity(pcm16Bytes) +RunAnywhere.streamVAD(audioFlow).collect { println(it.isSpeech) } +RunAnywhere.resetVAD() +``` + +## VLM (vision) + +```kotlin +val image = VLMImage.fromBitmap(bitmap) +val out = RunAnywhere.processImage(image, VLMGenerationOptions.defaults(prompt = "Describe this.")) + +// Streaming — prompt inline or in options +RunAnywhere.processImageStream(image, prompt = "What is this?").collect { print(it) } +RunAnywhere.cancelVLMGeneration() +``` + +## RAG + +```kotlin +RunAnywhere.ragCreatePipeline(embeddingModel, llmModel) +RunAnywhere.ragIngest(RARAGDocument(/* ... */)) +val answer = RunAnywhere.ragQuery("What does the doc say about pricing?") +RunAnywhere.ragQueryStream("Summarize section 2").collect { print(it) } +RunAnywhere.ragCancelQuery() +``` + +## LoRA + +```kotlin +RunAnywhere.lora.apply(catalogEntry, scale = 1.0f) +RunAnywhere.lora.applyCatalogAdapter(catalogEntry) // named alias +val state = RunAnywhere.lora.list() +RunAnywhere.lora.download(catalogEntry) { p -> println(p.percentage) } +``` + +## Voice agent + +```kotlin +RunAnywhere.initializeVoiceAgentWithLoadedModels() +RunAnywhere.streamVoiceAgent().collect { event -> println(event) } +val turn = RunAnywhere.processVoiceTurn(pcm16Bytes) +RunAnywhere.cleanupVoiceAgent() +``` + +## Events + +```kotlin +RunAnywhere.events.llmEvents.collect { println(it) } +RunAnywhere.events.modelLoaded.collect { println("loaded ${it.modelId}") } + +val subId = RunAnywhere.subscribeSDKEvents { event -> println(event) } +RunAnywhere.unsubscribeSDKEvents(subId) +``` + +## Notes + +- All inference calls are `suspend`; streaming returns `Flow`. +- Android-only capability: image generation / `inpaint` runs on the Qualcomm NPU (qhexrt) — not present on iOS. +- Diffusion, when a model is loaded: `RunAnywhere.generateImage(options)` and `RunAnywhere.inpaint(...)`. diff --git a/sdk/runanywhere-commons/docs/md/react-native.md b/sdk/runanywhere-commons/docs/md/react-native.md new file mode 100644 index 0000000000..006ac9bce2 --- /dev/null +++ b/sdk/runanywhere-commons/docs/md/react-native.md @@ -0,0 +1,136 @@ +# RunAnywhere React Native SDK — Public API Usage + +iOS + Android via NitroModules (JSI). Entry point is the `RunAnywhere` object. Async calls return `Promise`; streaming returns `AsyncIterable` consumed with a manual `iterator.next()` loop (Hermes does not support `for await` over Nitro async iterables). + +## Initialization + +```ts +import { RunAnywhere, SDKEnvironment } from '@runanywhere/core'; + +// Single options-bag initialize (carries RN-only phase-2 knobs too) +await RunAnywhere.initialize({ + apiKey: 'ra_...', // optional in development + environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, +}); +await RunAnywhere.completeServicesInitialization(); + +// State (bridge reads are async → Promise) +RunAnywhere.isInitialized; +RunAnywhere.areServicesReady; +RunAnywhere.version; +await RunAnywhere.deviceId; // Promise +await RunAnywhere.isAuthenticated(); +``` + +## Models + +```ts +const models = await RunAnywhere.listModels(); +const downloaded = await RunAnywhere.downloadedModels(); +const one = await RunAnywhere.getModel({ modelId: 'qwen2.5-0.5b' }); + +const info = await RunAnywhere.registerModel({ + name: 'Qwen2.5 0.5B', + url: 'https://huggingface.co/.../model.gguf', + framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP, +}); + +await RunAnywhere.downloadModel(info, (p) => console.log(p.percentage)); +await RunAnywhere.loadModel({ modelId: info.id }); +``` + +## LLM — text generation + +```ts +const result = await RunAnywhere.generate('Explain vector databases in one line.'); +console.log(result.text); + +// Streaming — manual iterator loop (Hermes-safe) +const it = RunAnywhere.generateStream('Write a haiku.')[Symbol.asyncIterator](); +for (let r = await it.next(); !r.done; r = await it.next()) { + process.stdout.write(r.value.token ?? ''); +} + +await RunAnywhere.cancelGeneration(); +``` + +### Structured output & tools + +```ts +const structured = await RunAnywhere.generateStructured('Extract', schema); +await RunAnywhere.registerTool(def, async (args) => ({ result: { stringValue: 'ok' } })); +const toolResult = await RunAnywhere.generateWithTools('Weather in Pune?', undefined, { + signal: abortController.signal, // RN uses AbortSignal for cancellation +}); +``` + +## STT / TTS / VAD + +```ts +const transcript = await RunAnywhere.transcribe(pcm16); +const audio = await RunAnywhere.synthesize('Hello there'); +await RunAnywhere.speak('Spoken aloud'); +await RunAnywhere.stopSpeaking(); + +const vad = await RunAnywhere.detectVoiceActivity(pcm16); +await RunAnywhere.resetVAD(); +``` + +## VLM (vision) + +```ts +const out = await RunAnywhere.processImage(image, { prompt: 'Describe this.' }); + +// prompt overload — applied onto options.prompt +const stream = await RunAnywhere.processImageStream(image, 'What is this?'); +const vit = stream[Symbol.asyncIterator](); +for (let r = await vit.next(); !r.done; r = await vit.next()) console.log(r.value); + +await RunAnywhere.cancelVLMGeneration(); +``` + +## RAG + +```ts +await RunAnywhere.ragCreatePipeline({ embeddingModel: emb, llmModel: llm }); +await RunAnywhere.ragIngest(document); +const answer = await RunAnywhere.ragQuery('What about pricing?'); +const rit = RunAnywhere.ragQueryStream('Summarize')[Symbol.asyncIterator](); +for (let r = await rit.next(); !r.done; r = await rit.next()) console.log(r.value); +``` + +## LoRA + +```ts +await RunAnywhere.lora.applyCatalogAdapter(entry, { scale: 1.0 }); +await RunAnywhere.lora.apply(entry); // catalog-entry overload +const state = await RunAnywhere.lora.list(); +await RunAnywhere.lora.download(entry, (p) => console.log(p)); +``` + +## Voice agent + +```ts +await RunAnywhere.initializeVoiceAgentWithLoadedModels(); +const vit = RunAnywhere.streamVoiceAgent()[Symbol.asyncIterator](); +for (let r = await vit.next(); !r.done; r = await vit.next()) console.log(r.value); +const turn = await RunAnywhere.processVoiceTurn(pcm16); +await RunAnywhere.cleanupVoiceAgent(); +``` + +## Events + +```ts +RunAnywhere.events.on((event) => console.log(event)); + +// Imperative SDK events — subscribe returns a numeric id +const id = await RunAnywhere.subscribeSDKEvents((event) => console.log(event)); +await RunAnywhere.unsubscribeSDKEvents(id); +``` + +## Notes + +- All streams are `AsyncIterable`; use manual `iterator.next()` loops (never `for await` — Hermes limitation). +- Cancellation uses `AbortSignal` (passed via the `extra` bag on `generateWithTools`, etc.). +- Web-search tool is not available on RN (RN core routes HTTP through native, not JS). +- Diffusion is Apple-gated; on non-Apple it throws. diff --git a/sdk/runanywhere-commons/docs/md/swift.md b/sdk/runanywhere-commons/docs/md/swift.md new file mode 100644 index 0000000000..f6b8378294 --- /dev/null +++ b/sdk/runanywhere-commons/docs/md/swift.md @@ -0,0 +1,155 @@ +# RunAnywhere Swift SDK — Public API Usage + +iOS 17.5+ / macOS 14.5+. Entry point is `enum RunAnywhere`; features are `static` methods in `public extension` blocks. This is the canonical surface every other SDK mirrors. Structured types are `RA*` typealiases to generated protos. + +## Initialization + +```swift +import RunAnywhere + +// Phase 1 — synchronous +try RunAnywhere.initialize( + apiKey: "ra_...", // optional in development + baseURL: nil, + environment: .development +) + +// Phase 2 — async services +try await RunAnywhere.completeServicesInitialization() + +// State +RunAnywhere.isInitialized +RunAnywhere.areServicesReady +RunAnywhere.version +let id = try RunAnywhere.deviceId // throwing property +RunAnywhere.isAuthenticated +``` + +## Models + +```swift +let models = await RunAnywhere.listModels() +let downloaded = await RunAnywhere.downloadedModels() +let one = await RunAnywhere.getModel(RAModelGetRequest(id: "qwen2.5-0.5b")) + +let info = try await RunAnywhere.registerModel( + name: "Qwen2.5 0.5B", + url: "https://huggingface.co/.../model.gguf", + framework: .llamaCpp +) + +try await RunAnywhere.downloadModel(info) { progress in print(progress.percentage) } +_ = await RunAnywhere.loadModel(RAModelLoadRequest(modelID: info.id)) + +for await progress in RunAnywhere.downloadModelStream(info) { print(progress.state) } +``` + +## LLM — text generation + +```swift +let result = try await RunAnywhere.generate(prompt: "Explain vector databases in one line.") +print(result.text) + +for await event in try await RunAnywhere.generateStream(prompt: "Write a haiku.") { + if let token = event.token { print(token, terminator: "") } +} + +await RunAnywhere.cancelGeneration() +``` + +### Structured output + +```swift +let structured = try await RunAnywhere.generateStructured(prompt: "Extract fields", schema: schema) +let extracted = try RunAnywhere.extractStructuredOutput(text: raw, schema: schema) +``` + +### Tool calling + +```swift +await RunAnywhere.registerTool(toolDefinition) { args in ["result": RAToolValue("ok")] } +let toolResult = try await RunAnywhere.generateWithTools(prompt: "Weather in Pune?") + +// Built-in web search tool (DuckDuckGo, over URLSession) +await RunAnywhere.registerWebSearchTool() +let def = RunAnywhere.webSearchToolDefinition +``` + +## STT / TTS / VAD + +```swift +let transcript = try await RunAnywhere.transcribe(audio: data) +for await partial in RunAnywhere.transcribeStream(audio: audioStream) { print(partial.text) } + +let audio = try await RunAnywhere.synthesize("Hello there") +_ = try await RunAnywhere.speak("Spoken aloud") +await RunAnywhere.stopSpeaking() + +let vad = try await RunAnywhere.detectVoiceActivity(data) +for await r in RunAnywhere.streamVAD(audio: audioStream) { print(r.isSpeech) } +try await RunAnywhere.resetVAD() +``` + +## VLM (vision) + +```swift +let out = try await RunAnywhere.processImage(image, options: .defaults()) + +for await event in try await RunAnywhere.processImageStream(image, prompt: "Describe this.") { + print(event) +} +await RunAnywhere.cancelVLMGeneration() +``` + +## Diffusion (Apple / CoreML only) + +```swift +let image = try await RunAnywhere.generateImage(options) +for await event in try await RunAnywhere.generateImageStream(options) { print(event) } +await RunAnywhere.cancelImageGeneration() +``` + +## RAG + +```swift +try await RunAnywhere.ragCreatePipeline(embeddingModel: emb, llmModel: llm) +try await RunAnywhere.ragIngest(document) +let answer = try await RunAnywhere.ragQuery(question: "What about pricing?") +for await event in try await RunAnywhere.ragQueryStream(question: "Summarize") { print(event) } +await RunAnywhere.ragCancelQuery() // session-scoped cancel +``` + +## LoRA + +```swift +try await RunAnywhere.lora.apply(catalogEntry, scale: 1.0) +try await RunAnywhere.lora.applyCatalogAdapter(catalogEntry) +let state = try await RunAnywhere.lora.list() +_ = try await RunAnywhere.lora.download(catalogEntry) { p in print(p.percentage) } +``` + +## Voice agent + +```swift +try await RunAnywhere.initializeVoiceAgentWithLoadedModels() +for await event in RunAnywhere.streamVoiceAgent() { print(event) } +let turn = try await RunAnywhere.processVoiceTurn(data) +await RunAnywhere.cleanupVoiceAgent() +``` + +## Events + +```swift +RunAnywhere.events.llmEvents.sink { print($0) }.store(in: &cancellables) +RunAnywhere.events.modelLoaded.sink { print("loaded \($0.modelId)") }.store(in: &cancellables) + +let id = RunAnywhere.subscribeSDKEvents { event in print(event) } +RunAnywhere.unsubscribeSDKEvents(id) +``` + +## Notes + +- Inference calls are `async`; some are `throws`. Streaming returns `AsyncStream`. +- `deviceId` is a throwing computed property. +- Diffusion is Apple/CoreML only (no `inpaint` convenience on the facade; use `generateImage`). +- Events use Combine publishers. diff --git a/sdk/runanywhere-commons/docs/md/web.md b/sdk/runanywhere-commons/docs/md/web.md new file mode 100644 index 0000000000..f1ce3f1f4f --- /dev/null +++ b/sdk/runanywhere-commons/docs/md/web.md @@ -0,0 +1,145 @@ +# RunAnywhere Web SDK — Public API Usage + +Browsers via Emscripten WASM + TypeScript. Entry point is the `RunAnywhere` object (flat facade). Async calls return `Promise`; streaming returns `AsyncIterable`. Requires cross-origin isolation (COOP/COEP) for `SharedArrayBuffer`. + +## Initialization + +```ts +import { RunAnywhere, SDKEnvironment } from '@runanywhere/web'; + +await RunAnywhere.initialize({ + apiKey: 'ra_...', // optional in development + environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, +}); +await RunAnywhere.completeServicesInitialization(); + +// State +RunAnywhere.isInitialized; +RunAnywhere.areServicesReady; +RunAnywhere.version; +RunAnywhere.deviceId; // throwing getter +RunAnywhere.isAuthenticated; + +// Web-only: acceleration runtime + browser storage +RunAnywhere.setRuntime('webgpu'); // or 'wasm' +await RunAnywhere.hydrateModelRegistry(); +``` + +## Models + +```ts +const models = RunAnywhere.listModels(); +const downloaded = RunAnywhere.downloadedModels(); +const one = RunAnywhere.getModel('qwen2.5-0.5b'); +const framework = RunAnywhere.getDefaultFramework(ModelCategory.MODEL_CATEGORY_LANGUAGE); +const role = RunAnywhere.inferModelFileRole('mmproj.gguf', ModelCategory.MODEL_CATEGORY_MULTIMODAL); + +const info = RunAnywhere.registerModel('https://.../model.gguf', 'Qwen2.5 0.5B', framework); + +await RunAnywhere.downloadModel(info.id, { onProgress: (p) => console.log(p.percentage) }); +await RunAnywhere.loadModel({ modelId: info.id }); + +for await (const p of RunAnywhere.downloadModelStream(info.id)) console.log(p.state); +``` + +## LLM — text generation + +```ts +const result = await RunAnywhere.generate({ prompt: 'Explain vector databases in one line.' }); +console.log(result.text); + +// generateStream returns { events, stream, result, cancel } +const streaming = await RunAnywhere.generateStream({ prompt: 'Write a haiku.' }); +for await (const token of streaming.stream) process.stdout.write(token); +const final = await streaming.result; + +RunAnywhere.cancelGeneration(); +``` + +### Structured output & tools + +```ts +const structured = await RunAnywhere.generateStructured('Extract', schema); +RunAnywhere.registerTool(def, async (args) => ({ result: { stringValue: 'ok' } })); +const toolResult = await RunAnywhere.generateWithTools('Weather in Pune?', undefined, { + signal: abortController.signal, +}); +``` + +## STT / TTS / VAD + +```ts +const transcript = await RunAnywhere.transcribe(pcm16); +for await (const p of RunAnywhere.transcribeStream(audioStream)) console.log(p.text); + +const audio = await RunAnywhere.synthesize('Hello there'); +for await (const chunk of RunAnywhere.synthesizeStream('Streamed speech')) { /* ... */ } +await RunAnywhere.speak('Spoken aloud'); +RunAnywhere.stopSpeaking(); +RunAnywhere.stopSynthesis(); + +const vad = await RunAnywhere.detectVoiceActivity(float32); +for await (const r of RunAnywhere.streamVAD(frameStream)) console.log(r.isSpeech); +RunAnywhere.resetVAD(); +``` + +## VLM (vision) + +```ts +const out = await RunAnywhere.processImage(image, options); + +// prompt overload +const stream = await RunAnywhere.processImageStream(image, 'Describe this.'); +for await (const event of stream) console.log(event); + +await RunAnywhere.cancelVLMGeneration(); +``` + +## RAG + +```ts +await RunAnywhere.ragCreatePipeline(embeddingModelId, llmModelId); +await RunAnywhere.ragIngest(document); +const answer = await RunAnywhere.ragQuery('What about pricing?'); +for await (const event of RunAnywhere.ragQueryStream('Summarize')) console.log(event); +const count = await RunAnywhere.ragDocumentCount(); +``` + +## LoRA + +```ts +await RunAnywhere.lora.applyCatalogAdapter(entry, { scale: 1.0 }); +const state = await RunAnywhere.lora.list(); +await RunAnywhere.lora.download(entry, (p) => console.log(p)); +// Browser import takes a File/Blob +await RunAnywhere.lora.importAdapter(file, 'adapter.bin'); +``` + +## Voice agent + +```ts +await RunAnywhere.initializeVoiceAgentWithLoadedModels(); +for await (const event of RunAnywhere.streamVoiceAgent()) console.log(event); +const turn = await RunAnywhere.processVoiceTurn(float32); +await RunAnywhere.cleanupVoiceAgent(); +``` + +## Events + +```ts +// Flat aliases (Swift-named) delegate to the sdkEvents adapter +const unsubscribe = RunAnywhere.subscribeSDKEvents((event) => console.log(event)); +unsubscribe?.(); +RunAnywhere.publishSDKEvent(event); +const next = RunAnywhere.pollSDKEvent(); + +// Reactive EventBus +RunAnywhere.events.modelLoaded; // async-iterable stream +``` + +## Notes + +- `generateStream` returns a rich `LLMStreamingResult` (`events`, `stream`, `result`, `cancel`) rather than a bare iterable. +- Component-handle namespaces (`RunAnywhere.stt/tts/vad.*`) exist for advanced use; the flat methods are lifecycle-auto (no handle). +- Web-only surface: browser storage / OPFS (`RunAnywhere.storage.*`), runtime modes, hybrid STT (`RunAnywhere.hybrid.*`). +- Not available on Web: `setHfToken`, web-search tool, standalone id-based `unsubscribeSDKEvents` (use the closure returned by `subscribeSDKEvents`). Diffusion is Apple-only. diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index e3c42429fd..4279bdaa32 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -32,13 +32,27 @@ import 'package:runanywhere/generated/model_types.pb.dart' show CurrentModelRequest, CurrentModelResult, + ModelFileDescriptor, + ModelGetRequest, + ModelGetResult, + ModelImportRequest, + ModelImportResult, ModelInfo, + ModelListResult, ModelLoadRequest, ModelLoadResult, + ModelQuery, ModelUnloadRequest, ModelUnloadResult; import 'package:runanywhere/generated/model_types.pbenum.dart' - show InferenceFramework, ModelCategory; + show + ArchiveStructure, + ArchiveType, + InferenceFramework, + ModelArtifactType, + ModelCategory, + ModelFileRole, + ModelSource; import 'package:runanywhere/generated/rag.pb.dart' show RAGConfiguration, @@ -51,7 +65,11 @@ import 'package:runanywhere/generated/sdk_events.pb.dart' as sdk_events_pb; import 'package:runanywhere/generated/sdk_events.pbenum.dart' show SDKComponent; import 'package:runanywhere/generated/sdk_init.pb.dart' show SdkInitResult; import 'package:runanywhere/generated/storage_types.pb.dart' - show StorageDeleteRequest, StorageDeleteResult; + show + StorageDeleteRequest, + StorageDeleteResult, + StorageInfoRequest, + StorageInfoResult; import 'package:runanywhere/generated/structured_output.pb.dart' show JSONSchema, @@ -69,7 +87,14 @@ import 'package:runanywhere/generated/tool_calling.pb.dart' ToolResult; import 'package:runanywhere/generated/tts_options.pb.dart' show TTSOptions, TTSOutput, TTSSpeakResult; -import 'package:runanywhere/generated/voice_events.pb.dart' show VoiceEvent; +import 'package:runanywhere/generated/vad_options.pb.dart' + show VADOptions, VADResult; +import 'package:runanywhere/generated/vlm_options.pb.dart' + show VLMGenerationOptions, VLMImage, VLMResult, VLMStreamEvent; +import 'package:runanywhere/generated/voice_agent_service.pb.dart' + show VoiceAgentComposeConfig, VoiceAgentResult; +import 'package:runanywhere/generated/voice_events.pb.dart' + show VoiceAgentComponentStates, VoiceEvent; import 'package:runanywhere/native/dart_bridge.dart'; import 'package:runanywhere/native/dart_bridge_auth.dart'; import 'package:runanywhere/native/dart_bridge_device.dart'; @@ -1171,4 +1196,236 @@ abstract final class RunAnywhere { /// Mirrors Swift `RunAnywhere.cancelImageGeneration()`. static Future cancelImageGeneration() => RunAnywhereDiffusion.shared.cancelImageGeneration(); + + // --- Flat aliases: VAD (mirror Swift's flat RunAnywhere.* surface) ---------- + + /// Flat alias — detect voice activity in a PCM16 buffer. + /// Mirrors Swift `RunAnywhere.detectVoiceActivity(_:options:)`. + static Future detectVoiceActivity( + Uint8List audio, [ + VADOptions? options, + ]) => RunAnywhereVAD.shared.detectVoiceActivity(audio, options); + + /// Flat alias — stream voice-activity results over a PCM16 chunk stream. + /// Mirrors Swift `RunAnywhere.streamVAD(audio:)`. + static Stream streamVAD(Stream audio) => + RunAnywhereVAD.shared.streamVAD(audio); + + /// Flat alias — reset VAD state. Mirrors Swift `RunAnywhere.resetVAD()`. + static void resetVAD() => RunAnywhereVAD.shared.reset(); + + // --- Flat aliases: VLM ------------------------------------------------------ + + /// Flat alias — process an image with the loaded VLM. [prompt] is applied + /// onto the options when unset. Mirrors Swift + /// `RunAnywhere.processImage(_:options:)`. + static Future processImage( + VLMImage image, { + String? prompt, + VLMGenerationOptions? options, + }) => RunAnywhereVLM.shared.processImage( + image, + prompt: prompt, + options: options, + ); + + /// Flat alias — stream VLM generation events. Mirrors Swift + /// `RunAnywhere.processImageStream(_:options:)` and its prompt overload. + static Stream processImageStream( + VLMImage image, { + String? prompt, + VLMGenerationOptions? options, + }) => RunAnywhereVLM.shared.processImageStream( + image, + prompt: prompt, + options: options, + ); + + /// Flat alias — cancel the in-flight VLM generation. + /// Mirrors Swift `RunAnywhere.cancelVLMGeneration()`. + static Future cancelVLMGeneration() => + RunAnywhereVLM.shared.cancelVLMGeneration(); + + // --- Flat aliases: TTS streaming -------------------------------------------- + + /// Flat alias — stream synthesized audio chunks. + /// Mirrors Swift `RunAnywhere.synthesizeStream(_:options:)`. + static Stream synthesizeStream( + String text, { + TTSOptions? options, + }) => RunAnywhereTTS.shared.synthesizeStream(text, options: options); + + /// Flat alias — stop TTS playback. Mirrors Swift `RunAnywhere.stopSpeaking()`. + static Future stopSpeaking() => RunAnywhereTTS.shared.stopSpeaking(); + + // --- Flat aliases: Voice Agent ---------------------------------------------- + + /// Default catalogued VAD model id. Mirrors Swift + /// `RunAnywhere.defaultVADModelID`. + static String get defaultVADModelID => + RunAnywhereVoice.shared.defaultVADModelID; + + /// Flat alias — ensure the default VAD model is available. + /// Mirrors Swift `RunAnywhere.ensureDefaultVAD(modelID:)`. + static Future ensureDefaultVAD({String? modelID}) => + RunAnywhereVoice.shared.ensureDefaultVAD(modelID: modelID); + + /// Flat alias — initialize the voice agent from a compose config. + /// Mirrors Swift `RunAnywhere.initializeVoiceAgent(_:)`. + static Future initializeVoiceAgent(VoiceAgentComposeConfig config) => + RunAnywhereVoice.shared.initializeVoiceAgent(config); + + /// Flat alias — initialize the voice agent over already-loaded models. + /// Mirrors Swift `RunAnywhere.initializeVoiceAgentWithLoadedModels(...)`. + static Future initializeVoiceAgentWithLoadedModels({ + String? ttsVoiceID, + bool ensureVAD = true, + }) => RunAnywhereVoice.shared.initializeWithLoadedModels( + ttsVoiceID: ttsVoiceID, + ensureVAD: ensureVAD, + ); + + /// Flat alias — read the voice agent's per-component load states. + /// Mirrors Swift `RunAnywhere.getVoiceAgentComponentStates()`. + static Future getVoiceAgentComponentStates() => + RunAnywhereVoice.shared.componentStates(); + + /// Flat alias — run a one-shot voice turn (audio in → result out). + /// Mirrors Swift `RunAnywhere.processVoiceTurn(_:)`. + static Future processVoiceTurn(Uint8List audioData) => + RunAnywhereVoice.shared.processVoiceTurn(audioData); + + /// Flat alias — tear down voice agent native resources. + /// Mirrors Swift `RunAnywhere.cleanupVoiceAgent()`. + static void cleanupVoiceAgent() => RunAnywhereVoice.shared.cleanup(); + + // --- Flat aliases: Model registry ------------------------------------------- + + /// Flat alias — list registered models. Mirrors Swift + /// `RunAnywhere.listModels(_:)`. + static Future listModels({ModelQuery? query}) => + RunAnywhereModels.shared.list(query: query); + + /// Flat alias — query models with a generated filter. + /// Mirrors Swift `RunAnywhere.queryModels(_:)`. + static Future queryModels(ModelQuery query) => + RunAnywhereModels.shared.queryModels(query); + + /// Flat alias — fetch one model by generated request. + /// Mirrors Swift `RunAnywhere.getModel(_:)`. + static Future getModel(ModelGetRequest request) => + RunAnywhereModels.shared.getModel(request); + + /// Flat alias — list downloaded models. + /// Mirrors Swift `RunAnywhere.downloadedModels()`. + static Future downloadedModels() => + RunAnywhereModels.shared.downloadedModels(); + + /// Flat alias — infer a model file's role from its name + modality. + /// Mirrors Swift `RunAnywhere.inferModelFileRole(filename:modality:)`. + static ModelFileRole inferModelFileRole({ + required String filename, + required ModelCategory modality, + }) => RunAnywhereModels.shared.inferModelFileRole( + filename: filename, + modality: modality, + ); + + // --- Flat alias: streaming download ----------------------------------------- + + /// Flat alias — stream download progress for a model. + /// Mirrors Swift `RunAnywhere.downloadModelStream(_:)`. + static Stream downloadModelStream(String modelId) => + RunAnywhereDownloads.shared.start(modelId); + + // --- Flat aliases: Storage / model registration ----------------------------- + + /// Flat alias — register a single-file remote model by URL. + /// Mirrors Swift `RunAnywhere.registerModel(id:name:url:framework:...)`. + static Future registerModel({ + String? id, + required String name, + required String url, + required InferenceFramework framework, + ModelCategory modality = ModelCategory.MODEL_CATEGORY_LANGUAGE, + ModelArtifactType? artifactType, + int? memoryRequirement, + bool supportsThinking = false, + bool supportsLora = false, + }) => RunAnywhereStorage.registerModel( + id: id, + name: name, + url: url, + framework: framework, + modality: modality, + artifactType: artifactType, + memoryRequirement: memoryRequirement, + supportsThinking: supportsThinking, + supportsLora: supportsLora, + ); + + /// Flat alias — register an archive-packaged model. + /// Mirrors Swift `RunAnywhere.registerModel(archive:structure:...)`. + static Future registerArchiveModel({ + required String archiveUrl, + required ArchiveStructure structure, + String? id, + required String name, + required InferenceFramework framework, + ModelCategory modality = ModelCategory.MODEL_CATEGORY_LANGUAGE, + ArchiveType? archiveType, + int? memoryRequirement, + bool supportsThinking = false, + bool supportsLora = false, + }) => RunAnywhereStorage.registerArchiveModel( + archiveUrl: archiveUrl, + structure: structure, + id: id, + name: name, + framework: framework, + modality: modality, + archiveType: archiveType, + memoryRequirement: memoryRequirement, + supportsThinking: supportsThinking, + supportsLora: supportsLora, + ); + + /// Flat alias — register a multi-file model. + /// Mirrors Swift `RunAnywhere.registerModel(multiFile:id:name:framework:...)`. + static Future registerMultiFileModel({ + required List files, + required String id, + required String name, + required InferenceFramework framework, + ModelCategory modality = ModelCategory.MODEL_CATEGORY_LANGUAGE, + int? memoryRequirement, + int? contextLength, + bool supportsThinking = false, + ModelSource source = ModelSource.MODEL_SOURCE_REMOTE, + }) => RunAnywhereStorage.registerMultiFileModel( + files: files, + id: id, + name: name, + framework: framework, + modality: modality, + memoryRequirement: memoryRequirement, + contextLength: contextLength, + supportsThinking: supportsThinking, + source: source, + ); + + /// Flat alias — import a local model into the registry. + /// Mirrors Swift `RunAnywhere.importModel(_:)`. + static Future importModel(ModelImportRequest request) => + RunAnywhereStorage.importModel(request); + + /// Flat alias — read storage info as a generated result. + /// Mirrors Swift `RunAnywhere.getStorageInfo(_:)`. + static Future getStorageInfo([ + StorageInfoRequest? request, + ]) => RunAnywhereDownloads.shared.getStorageInfoResult(request); + + /// Flat alias — clear the model/download cache. + /// Mirrors Swift `RunAnywhere.clearCache()`. + static Future clearCache() => RunAnywhereDownloads.shared.clearCache(); } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.kt index 9fefbd9162..d2faac9d8a 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.kt @@ -93,6 +93,17 @@ interface LoRA { ) } + /** + * Named alias for [apply] on a catalog entry, mirroring Swift + * `applyCatalogAdapter(_:localPath:scale:replaceExisting:)`. + */ + suspend fun applyCatalogAdapter( + entry: LoraAdapterCatalogEntry, + localPath: String? = null, + scale: Float? = null, + replaceExisting: Boolean = false, + ): LoRAApplyResult = apply(entry, localPath, scale, replaceExisting) + /** Remove adapters by generated request semantics, including `clear_all`. */ suspend fun remove(request: RALoRARemoveRequest): RALoRAState diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt new file mode 100644 index 0000000000..a52066b489 --- /dev/null +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt @@ -0,0 +1,364 @@ +/* + * Copyright 2026 RunAnywhere SDK + * SPDX-License-Identifier: Apache-2.0 + * + * SDK-facing web search tool helper for tool-calling clients. + * + * Mirrors Swift sdk/runanywhere-swift/.../RunAnywhere+WebSearchTool.swift 1:1: + * a built-in `search_web` tool backed by DuckDuckGo (lite HTML results with an + * Instant Answer API fallback), registered through the existing + * `registerTool` / `ToolExecutor` surface. OkHttp is the platform transport + * (URLSession on Swift). + */ + +package com.runanywhere.sdk.public.extensions + +import ai.runanywhere.proto.v1.ToolDefinition +import ai.runanywhere.proto.v1.ToolParameter +import ai.runanywhere.proto.v1.ToolParameterType +import ai.runanywhere.proto.v1.ToolValue +import ai.runanywhere.proto.v1.ToolValueArray +import ai.runanywhere.proto.v1.ToolValueObject +import com.runanywhere.sdk.public.RunAnywhere +import com.runanywhere.sdk.public.extensions.LLM.ToolExecutor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.URLDecoder +import java.util.concurrent.TimeUnit + +/** Definition for the built-in web search tool helper. */ +val RunAnywhere.webSearchToolDefinition: ToolDefinition + get() = WebSearchTool.definition + +/** Register the built-in web search tool helper. */ +suspend fun RunAnywhere.registerWebSearchTool() { + registerTool(WebSearchTool.definition, WebSearchTool.executor) +} + +private object WebSearchTool { + private object Tool { + const val NAME = "search_web" + const val DESCRIPTION = + "Searches the web for current information using DuckDuckGo Instant Answer API" + const val CATEGORY = "Web" + } + + private object Parameter { + const val QUERY = "query" + const val QUERY_DESCRIPTION = + "Search query (e.g., 'latest Kotlin coroutine updates')" + } + + private object PayloadKey { + const val ERROR = "error" + const val QUERY = "query" + const val HEADING = "heading" + const val SUMMARY = "summary" + const val SOURCE_URL = "source_url" + const val SEARCH_URL = "search_url" + const val RELATED_RESULTS = "related_results" + const val TITLE = "title" + const val TEXT = "text" + const val URL = "url" + } + + private const val USER_AGENT_HEADER = "User-Agent" + private const val USER_AGENT = "Mozilla/5.0" + private const val TIMEOUT_SECONDS = 15L + private const val MAX_RESULTS = 5 + private const val SNIPPET_TAIL_CHARS = 1_500 + + private val client = + OkHttpClient.Builder() + .callTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build() + + private val json = Json { ignoreUnknownKeys = true } + + val definition = + ToolDefinition( + name = Tool.NAME, + description = Tool.DESCRIPTION, + parameters = + listOf( + ToolParameter( + name = Parameter.QUERY, + type = ToolParameterType.TOOL_PARAMETER_TYPE_STRING, + description = Parameter.QUERY_DESCRIPTION, + ), + ), + category = Tool.CATEGORY, + ) + + val executor: ToolExecutor = { args -> + search(args[Parameter.QUERY]?.string_value ?: "") + } + + private suspend fun search(rawQuery: String): Map { + val query = rawQuery.trim() + if (query.isEmpty()) { + return mapOf(PayloadKey.ERROR to str("Missing search query")) + } + val liteUrl = + makeLiteSearchURL(query) + ?: return mapOf(PayloadKey.ERROR to str("Invalid search query")) + + val html = httpGet(liteUrl) + val results = parseLiteResults(html).take(MAX_RESULTS) + val first = results.firstOrNull() + if (first != null) { + return resultPayload(query, first, results.drop(1)) + } + return instantAnswerFallback(query) + } + + private suspend fun instantAnswerFallback(query: String): Map { + val url = + makeInstantAnswerURL(query) + ?: return mapOf(PayloadKey.ERROR to str("Invalid search query")) + + val body = httpGet(url) + val root = + runCatching { json.parseToJsonElement(body) as? JsonObject }.getOrNull() + ?: return mapOf(PayloadKey.ERROR to str("Could not parse search response")) + + val abstractText = root.stringField("AbstractText") + val answer = root.stringField("Answer") + val heading = root.stringField("Heading") + val abstractURL = root.stringField("AbstractURL") + val related = collectRelatedTopics(root["RelatedTopics"]).take(MAX_RESULTS) + + val result = linkedMapOf(PayloadKey.QUERY to str(query)) + + if (heading.isNotEmpty()) { + result[PayloadKey.HEADING] = str(heading) + } + + result[PayloadKey.SUMMARY] = + str( + when { + abstractText.isNotEmpty() -> abstractText + answer.isNotEmpty() -> answer + related.isNotEmpty() -> related.first().text + else -> + "No direct answer was available. Open the search results for current sources." + }, + ) + + if (abstractURL.isNotEmpty()) { + result[PayloadKey.SOURCE_URL] = str(abstractURL) + } else { + makeSearchResultsURL(query)?.toString()?.let { searchURL -> + result[PayloadKey.SOURCE_URL] = str(searchURL) + result[PayloadKey.SEARCH_URL] = str(searchURL) + } + } + + if (related.isNotEmpty()) { + result[PayloadKey.RELATED_RESULTS] = + arr( + related.map { topic -> + obj( + mapOf( + PayloadKey.TITLE to str(topic.title), + PayloadKey.TEXT to str(topic.text), + PayloadKey.URL to str(topic.url), + ), + ) + }, + ) + } + + return result + } + + private suspend fun httpGet(url: HttpUrl): String = + withContext(Dispatchers.IO) { + val request = + Request.Builder() + .url(url) + .header(USER_AGENT_HEADER, USER_AGENT) + .build() + runCatching { + client.newCall(request).execute().use { response -> + response.body.string() + } + }.getOrDefault("") + } + + private fun resultPayload( + query: String, + primary: SearchResult, + related: List, + ): Map { + val result = + linkedMapOf( + PayloadKey.QUERY to str(query), + PayloadKey.HEADING to str(primary.title), + PayloadKey.SUMMARY to str(primary.snippet), + PayloadKey.SOURCE_URL to str(primary.url), + ) + if (related.isNotEmpty()) { + result[PayloadKey.RELATED_RESULTS] = + arr( + related.map { item -> + obj( + mapOf( + PayloadKey.TITLE to str(item.title), + PayloadKey.TEXT to str(item.snippet), + PayloadKey.URL to str(item.url), + ), + ) + }, + ) + } + return result + } + + private fun parseLiteResults(html: String): List = + resultLinkRegex.findAll(html).mapNotNull { match -> + val href = match.groupValues.getOrNull(1)?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null + val rawTitle = match.groupValues.getOrNull(2) ?: return@mapNotNull null + val resolvedURL = redirectURL(decodeHTML(href)) + val cleanTitle = cleanHTML(rawTitle) + if (cleanTitle.isEmpty() || resolvedURL.isEmpty()) return@mapNotNull null + val snippet = snippetAfter(match.range.last + 1, html) ?: cleanTitle + SearchResult(title = cleanTitle, url = resolvedURL, snippet = snippet) + }.toList() + + private fun snippetAfter(startIndex: Int, html: String): String? { + if (startIndex >= html.length) return null + val tail = html.substring(startIndex).take(SNIPPET_TAIL_CHARS) + val match = snippetRegex.find(tail) ?: return null + val raw = match.groupValues.getOrNull(1) ?: return null + return cleanHTML(raw).ifEmpty { null } + } + + private fun redirectURL(href: String): String { + val marker = "uddg=" + val index = href.indexOf(marker) + if (index < 0) { + return if (href.startsWith("//")) "https:$href" else href + } + val encoded = href.substring(index + marker.length).substringBefore("&") + return runCatching { URLDecoder.decode(encoded, "UTF-8") }.getOrDefault(encoded) + } + + private fun collectRelatedTopics(element: JsonElement?): List { + val array = element as? JsonArray ?: return emptyList() + return array.flatMap { item -> + val topic = item as? JsonObject ?: return@flatMap emptyList() + val nested = topic["Topics"] + if (nested is JsonArray && nested.isNotEmpty()) { + collectRelatedTopics(nested) + } else { + val text = topic.stringField("Text") + if (text.isEmpty()) { + emptyList() + } else { + listOf( + RelatedTopic( + title = text.substringBefore(" - "), + text = text, + url = topic.stringField("FirstURL"), + ), + ) + } + } + } + } + + private fun cleanHTML(value: String): String = + decodeHTML(value.replace(htmlTagRegex, " ")) + .replace(whitespaceRegex, " ") + .trim() + + private fun decodeHTML(value: String): String = + value + .replace("&", "&") + .replace(""", "\"") + .replace("'", "'") + .replace("'", "'") + .replace("<", "<") + .replace(">", ">") + .replace(" ", " ") + + private fun makeLiteSearchURL(query: String): HttpUrl? = + runCatching { + HttpUrl.Builder() + .scheme("https") + .host("lite.duckduckgo.com") + .addPathSegment("lite") + .addPathSegment("") + .addQueryParameter(Parameter.QUERY, query) + .build() + }.getOrNull() + + private fun makeInstantAnswerURL(query: String): HttpUrl? = + runCatching { + HttpUrl.Builder() + .scheme("https") + .host("api.duckduckgo.com") + .addQueryParameter("q", query) + .addQueryParameter("format", "json") + .addQueryParameter("no_redirect", "1") + .addQueryParameter("no_html", "1") + .addQueryParameter("skip_disambig", "1") + .build() + }.getOrNull() + + private fun makeSearchResultsURL(query: String): HttpUrl? = + runCatching { + HttpUrl.Builder() + .scheme("https") + .host("duckduckgo.com") + .addPathSegment("") + .addQueryParameter(Parameter.QUERY, query) + .build() + }.getOrNull() + + private fun str(value: String): ToolValue = ToolValue(string_value = value) + + private fun arr(values: List): ToolValue = + ToolValue(array_value = ToolValueArray(values = values)) + + private fun obj(fields: Map): ToolValue = + ToolValue(object_value = ToolValueObject(fields = fields)) + + private fun JsonObject.stringField(key: String): String = + (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.content?.trim().orEmpty() + + private val resultLinkRegex = + Regex( + """]*class=['"][^'"]*result-link[^'"]*['"])""" + + """(?=[^>]*href=['"]([^'"]+)['"])[^>]*>(.*?)""", + ) + + private val snippetRegex = + Regex("""]*class=['"][^'"]*result-snippet[^'"]*['"][^>]*>\s*([\s\S]*?)\s*""") + + private val htmlTagRegex = Regex("<[^>]+>") + private val whitespaceRegex = Regex("\\s+") + + private data class SearchResult( + val title: String, + val url: String, + val snippet: String, + ) + + private data class RelatedTopic( + val title: String, + val text: String, + val url: String, + ) +} diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VLM/RunAnywhereVisionLanguage.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VLM/RunAnywhereVisionLanguage.kt index 5d6fdbaafa..754a2534ef 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VLM/RunAnywhereVisionLanguage.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VLM/RunAnywhereVisionLanguage.kt @@ -157,6 +157,17 @@ fun RunAnywhere.processImageStream( } } +/** + * Ergonomic overload mirroring Swift `processImageStream(_:prompt:options:)` + * and React Native: the prompt is applied onto `options.prompt` before + * streaming. + */ +fun RunAnywhere.processImageStream( + image: RAVLMImage, + prompt: String, + options: RAVLMGenerationOptions = RAVLMGenerationOptions.defaults(), +): Flow = processImageStream(image, options.copy(prompt = prompt)) + // MARK: - Generation Control suspend fun RunAnywhere.cancelVLMGeneration() { diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Events/EventBus.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Events/EventBus.ts index 47ff7fc314..ab780376bd 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Events/EventBus.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Events/EventBus.ts @@ -32,8 +32,6 @@ const logger = new SDKLogger('EventBus'); export type SDKEventHandler = (event: SDKEventMessage) => void; export type EventBusCancellable = () => void; -type NativeUnsubscribe = () => Promise; - export class EventBus { private static readonly singleton = new EventBus(); @@ -43,7 +41,7 @@ export class EventBus { private readonly listeners = new Set(); private readonly categoryListeners = new Map>(); - private nativeSubscription: Promise | null = null; + private nativeSubscription: Promise | null = null; private constructor() { this.ensureNativeSubscription(); diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Events/RunAnywhere+SDKEvents.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Events/RunAnywhere+SDKEvents.ts index e95d68a15b..46c067ac3f 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Events/RunAnywhere+SDKEvents.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Events/RunAnywhere+SDKEvents.ts @@ -24,11 +24,13 @@ function decodeEvent(buffer: ArrayBuffer): SDKEventMessage | null { } /** - * Subscribe to native SDKEvent proto messages. + * Subscribe to native SDKEvent proto messages. Returns the native subscription + * id, which is passed to {@link unsubscribeSDKEvents} to stop the stream. + * Mirrors Swift `RunAnywhere.subscribeSDKEvents(_:) -> UInt64`. */ export async function subscribeSDKEvents( callback: (event: SDKEventMessage) => void -): Promise<() => Promise> { +): Promise { if (!isNativeModuleAvailable()) { throw SDKException.nativeModuleUnavailable(); } @@ -47,9 +49,23 @@ export async function subscribeSDKEvents( throw SDKException.generationFailedWith('Native SDKEvent subscription failed'); } - return async () => { - await native.unsubscribeSDKEventsProto(subscriptionId); - }; + return subscriptionId; +} + +/** + * Unsubscribe a previously-registered SDKEvent handler by the subscription id + * returned from {@link subscribeSDKEvents}. Mirrors Swift + * `RunAnywhere.unsubscribeSDKEvents(_:)`. No-op when native is unavailable. + */ +export async function unsubscribeSDKEvents( + subscriptionId: number +): Promise { + if (!isNativeModuleAvailable()) { + return; + } + + const native = requireNativeModule(); + await native.unsubscribeSDKEventsProto(subscriptionId); } /** diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts index e6e8cc2d82..94c6456665 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts @@ -167,8 +167,25 @@ function encodeDownloadCompletedRequest( /** * Apply one or more LoRA adapters to the current logical LLM session. + * + * Two forms mirror Swift's overloaded `apply`: a full `LoRAApplyRequest`, or a + * registered catalog entry (delegating to {@link applyCatalogAdapter}). */ -async function apply(request: LoRAApplyRequest): Promise { +function apply(request: LoRAApplyRequest): Promise; +function apply( + entry: LoraAdapterCatalogEntry, + options?: { localPath?: string; scale?: number; replaceExisting?: boolean } +): Promise; +async function apply( + requestOrEntry: LoRAApplyRequest | LoraAdapterCatalogEntry, + options?: { localPath?: string; scale?: number; replaceExisting?: boolean } +): Promise { + // A LoRAApplyRequest always carries an `adapters` array; a catalog entry does + // not — use that to route the entry form to applyCatalogAdapter. + if (!Array.isArray((requestOrEntry as LoRAApplyRequest).adapters)) { + return applyCatalogAdapter(requestOrEntry as LoraAdapterCatalogEntry, options); + } + const request = requestOrEntry as LoRAApplyRequest; const native = ensureNative(); const result = decodeRequired( await native.loraApplyProto(encodeApplyRequest(request)), diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts index fbc2354903..8a7339d23f 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts @@ -201,25 +201,6 @@ function deriveModelIdFromUrl(url: string, name: string): string { return normalized.length > 0 ? normalized : `model-${Date.now()}`; } -/** - * Register a single-file remote model by URL. Canonical entry point that - * mirrors Swift's `RunAnywhere.registerModel(id:name:url:framework:...)`, - * Kotlin's `RunAnywhere.registerModel(...)`, Flutter's - * `RunAnywhere.registerModel(...)`, and Web's `registerModelFromUrl(...)`. - * - * Delegates to `registerModel`, which builds a complete `ModelInfo` from the - * caller's input and persists it through the registry's proto save path in a - * single call. - */ -export async function registerModelFromUrl( - url: string, - name: string, - framework: InferenceFramework, - options: Omit = {}, -): Promise { - return registerModel({ url, name, framework, ...options }); -} - /** * Archive registration shorthand. Mirrors Swift's * `RunAnywhere.registerModel(archive:structure:id:name:framework:...)` diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.ts index 8d9b3284bb..0a7128477e 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.ts @@ -173,17 +173,16 @@ export async function deleteModel( * Clear the SDK's Temp directory. Mirrors Swift `cleanTempFiles()` → * `CppBridge.FileManager.clearTemp()`. */ -export async function cleanTempFiles(): Promise { +export async function cleanTempFiles(): Promise { if (!isNativeModuleAvailable()) { - return false; + return; } try { // Swift parity: RunAnywhere+Storage.swift:321 gates on ensureServicesReady. await ensureServicesReady(); const native = requireNativeModule(); - return await native.cleanTempFiles(); + await native.cleanTempFiles(); } catch (error) { logger.warning('Failed to clean temp files:', { error }); - return false; } } diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.ts index 9e6f72e8d3..653abc3fce 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.ts @@ -126,14 +126,29 @@ export async function processImage( /** * Stream image processing with canonical proto stream events. * - * Matches iOS: `RunAnywhere.processImageStream(_:options:)`, where - * `options.prompt` carries the prompt text. RN exposes the native VLM stream - * event proto as AsyncIterable. + * Matches iOS `RunAnywhere.processImageStream(_:options:)` and the ergonomic + * `processImageStream(_:prompt:options:)` overload: the prompt travels in + * `options.prompt`, and the prompt-first overload copies it there before + * streaming. RN exposes the native VLM stream event proto as AsyncIterable. */ -export async function processImageStream( +export function processImageStream( image: VLMImage, options: Partial +): Promise>; +export function processImageStream( + image: VLMImage, + prompt: string, + options?: Partial +): Promise>; +export async function processImageStream( + image: VLMImage, + optionsOrPrompt: Partial | string, + maybeOptions?: Partial ): Promise> { + const options: Partial = + typeof optionsOrPrompt === 'string' + ? { ...(maybeOptions ?? {}), prompt: optionsOrPrompt } + : optionsOrPrompt; // Swift parity: guard isInitialized (RunAnywhere+VisionLanguage.swift:56-58). requireInitialized(); const native = ensureNative(); diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts index 5ad1e0c6b5..3486904d25 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts @@ -616,40 +616,33 @@ export const RunAnywhere = { return native.isDeviceRegistered(); }, - /** - * Get device ID from native device state. - * - * Matches Swift's throwing `RunAnywhere.deviceId` property. RN returns a - * Promise because the value lives behind the Nitro async bridge and rejects - * if native identity cannot be resolved durably. - */ - async getDeviceId(): Promise { - if (!isNativeModuleAvailable()) { - throw SDKException.nativeModuleUnavailable(); - } - const native = requireNativeModule(); - try { - const registeredDeviceId = await native.getDeviceId(); - if (registeredDeviceId) return registeredDeviceId; - - const persistentDeviceId = await native.getPersistentDeviceUUID(); - if (!persistentDeviceId) { - throw SDKException.notInitialized('Persistent device identity'); - } - return persistentDeviceId; - } catch (error) { - throw await asNativeSDKException(error); - } - }, - /** * Device ID persisted in platform secure storage for the app installation. * - * RN-only property accessor — matches Swift's throwing device-ID getter, - * but returns `Promise` through the Nitro async native bridge. + * Mirrors Swift's single throwing `RunAnywhere.deviceId` accessor. RN returns + * a `Promise` because the value lives behind the Nitro async bridge + * (a synchronous getter is not expressible over it); the promise rejects if + * native identity cannot be resolved durably. */ get deviceId(): Promise { - return this.getDeviceId(); + return (async () => { + if (!isNativeModuleAvailable()) { + throw SDKException.nativeModuleUnavailable(); + } + const native = requireNativeModule(); + try { + const registeredDeviceId = await native.getDeviceId(); + if (registeredDeviceId) return registeredDeviceId; + + const persistentDeviceId = await native.getPersistentDeviceUUID(); + if (!persistentDeviceId) { + throw SDKException.notInitialized('Persistent device identity'); + } + return persistentDeviceId; + } catch (error) { + throw await asNativeSDKException(error); + } + })(); }, // ============================================================================ @@ -807,7 +800,6 @@ export const RunAnywhere = { // ============================================================================ registerModel: ModelManagement.registerModel, - registerModelFromUrl: ModelManagement.registerModelFromUrl, registerMultiFileModel: ModelManagement.registerMultiFileModel, registerArchiveModel: ModelManagement.registerArchiveModel, listModels: ModelManagement.listModels, @@ -842,6 +834,7 @@ export const RunAnywhere = { // ============================================================================ subscribeSDKEvents: SDKEvents.subscribeSDKEvents, + unsubscribeSDKEvents: SDKEvents.unsubscribeSDKEvents, publishSDKEvent: SDKEvents.publishSDKEvent, pollSDKEvent: SDKEvents.pollSDKEvent, publishSDKFailure: SDKEvents.publishSDKFailure, diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+RAG.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+RAG.swift index 0804b35e67..ccdf4568ed 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+RAG.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+RAG.swift @@ -14,9 +14,10 @@ import SwiftProtobuf /// Typed streaming ABI for `rac_rag_query_stream_proto`: takes a session handle /// plus a serialized `RAGQueryOptions` and emits serialized `RAGStreamEvent`s /// (TOKEN* → terminal COMPLETED/ERROR). The control callback returns -/// `rac_bool_t` — RAC_FALSE stops generation early (backpressure). RAG has no -/// separate per-query cancel symbol, so consumer cancellation is cooperative: -/// the callback returns RAC_FALSE and the native loop breaks on its next tick. +/// `rac_bool_t` — RAC_FALSE stops generation early (backpressure). Consumers +/// that break the stream (or cancel the owning task) stop generation +/// cooperatively via this return; `RAGCancelProtoABI` provides the explicit, +/// session-scoped imperative cancel used by `RunAnywhere.ragCancelQuery()`. private enum RAGStreamProtoABI { typealias StreamCallback = @convention(c) ( UnsafePointer?, @@ -35,6 +36,18 @@ private enum RAGStreamProtoABI { static let stream = NativeProtoABI.load(streamName, as: Stream.self) } +/// Session-scoped cancel ABI for `rac_rag_cancel_proto`: requests cancellation +/// of the query currently running on a RAG session. The active unary/streaming +/// run ends with an ERROR event carrying the cancellation status. Loaded +/// dynamically (RAG is a backend-conditional feature); `nil` when RAG is not +/// linked, in which case cancellation falls back to the cooperative path. +private enum RAGCancelProtoABI { + typealias Cancel = @convention(c) (rac_handle_t?) -> rac_result_t + + static let cancelName = "rac_rag_cancel_proto" + static let cancel = NativeProtoABI.load(cancelName, as: Cancel.self) +} + /// Retained RAG stream context released by the detached worker after the /// synchronous native stream call returns. private struct RAGStreamContextPointer: @unchecked Sendable { @@ -77,6 +90,22 @@ extension CppBridge { } } + /// Request cancellation of the query currently running on this session. + /// Session-scoped via `rac_rag_cancel_proto`; the active run ends with an + /// ERROR event. No-op when no session exists or RAG is not linked (the + /// stream's cooperative backpressure path still applies). + public func cancelActiveQuery() { + guard let protoSession else { return } + guard let cancel = RAGCancelProtoABI.cancel else { + logger.debug("rac_rag_cancel_proto unavailable; relying on cooperative cancellation") + return + } + let rc = cancel(protoSession) + if rc != RAC_SUCCESS { + logger.warning("rac_rag_cancel_proto failed: \(rc)") + } + } + private func setProtoSession(_ session: rac_handle_t) { if let existing = protoSession { destroyRAGProtoSessionIfAvailable(existing) diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/RAG/RunAnywhere+RAG.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/RAG/RunAnywhere+RAG.swift index 939b45f85a..b1f9e93b0b 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/RAG/RunAnywhere+RAG.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/RAG/RunAnywhere+RAG.swift @@ -197,6 +197,16 @@ public extension RunAnywhere { return try await CppBridge.RAG.shared.runQueryStream(options) } + + /// Immediately request cancellation of the active RAG query on the shared + /// session. Session-scoped, backed by `rac_rag_cancel_proto`: the in-flight + /// unary or streaming query ends with an ERROR event carrying the + /// cancellation status. Breaking out of a `ragQueryStream` already cancels + /// cooperatively — this is the explicit imperative form and mirrors the + /// cross-SDK `ragCancelQuery()` surface (Kotlin/React Native/Web). + static func ragCancelQuery() async { + await CppBridge.RAG.shared.cancelActiveQuery() + } } private extension RunAnywhere { diff --git a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+FlatFacade.ts b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+FlatFacade.ts index 54a5fb0a41..844c7579fe 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+FlatFacade.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+FlatFacade.ts @@ -63,6 +63,7 @@ import { streamVoiceAgent as streamVoiceAgentImpl, } from './RunAnywhere+VoiceAgent.js'; import { VisionLanguage as VisionLanguageCapability } from './RunAnywhere+VisionLanguage.js'; +import { SDKEvents } from './RunAnywhere+SDKEvents.js'; import { Logging as LoggingCapability } from './RunAnywhere+Logging.js'; import { pcm16ToFloat32 as pcm16ToFloat32Impl, @@ -135,6 +136,17 @@ export const flatFacade = { return ModelRegistryCapability.defaultFramework(category); }, + /** + * Mirrors Swift `RunAnywhere.inferModelFileRole(filename:modality:)` — the + * shared commons filename→role classifier, previously reachable on Web only + * internally via ProtoWasmBridge. + */ + inferModelFileRole( + ...args: Parameters + ): ReturnType { + return ModelRegistryCapability.inferModelFileRole(...args); + }, + /** * Mirrors Swift `RunAnywhere.refreshModelRegistry(rescanLocal:includeRemoteCatalog:pruneOrphans:)`. * Delegates to the internal `ModelRegistry` proto bridge. @@ -271,25 +283,25 @@ export const flatFacade = { return TTSCapability.synthesizeAuto(text, options); }, - stopSynthesis( - handle: Parameters[0], - ): ReturnType { - return TTSCapability.stop(handle); + /** + * Stop in-flight synthesis on the lifecycle-loaded TTS model. Parameterless + * to mirror Swift `RunAnywhere.stopSynthesis()`; the handle-owning form + * remains on `RunAnywhere.tts.stop(handle)`. + */ + stopSynthesis(): boolean { + return TTSCapability.stopLoaded(); }, /** * Stop current speech playback. Swift parity * (RunAnywhere+TTS.swift:133-136): stops the shared `speak()` browser - * playback first, then stops in-flight synthesis. The handle is optional — - * pass it only when using the handle-owning `RunAnywhere.tts.*` namespace. + * playback first, then stops in-flight synthesis on the lifecycle-loaded + * model. Parameterless to match Swift; the handle-owning stop stays on + * `RunAnywhere.tts.stop(handle)`. */ - stopSpeaking( - handle?: Parameters[0], - ): boolean { + stopSpeaking(): boolean { stopTTSPlaybackImpl(); - return handle !== undefined - ? TTSCapability.stop(handle) - : TTSCapability.stopLoaded(); + return TTSCapability.stopLoaded(); }, // ------------------------------------------------------------------------- @@ -302,10 +314,13 @@ export const flatFacade = { return VADCapability.detectVoiceAuto(...args); }, - resetVAD( - handle: Parameters[0], - ): ReturnType { - return VADCapability.reset(handle); + /** + * Reset the lifecycle-loaded VAD state. Parameterless to mirror Swift + * `RunAnywhere.resetVAD()`; the handle-owning form stays on + * `RunAnywhere.vad.reset(handle)`. + */ + resetVAD(): ReturnType { + return VADCapability.resetVoiceAuto(); }, // ------------------------------------------------------------------------- @@ -344,6 +359,13 @@ export const flatFacade = { return ragGetDocumentCountImpl(); }, + // Second accessor mirroring Swift's `ragDocumentCount` convenience alongside + // `ragGetDocumentCount()` (RunAnywhere+RAG.swift:108/139). Same underlying + // count; kept for cross-SDK surface parity (Kotlin/RN also expose both). + ragDocumentCount(): ReturnType { + return ragGetDocumentCountImpl(); + }, + ragGetStatistics(): ReturnType { return ragGetStatisticsImpl(); }, @@ -486,4 +508,36 @@ export const flatFacade = { ): ReturnType { return LoggingCapability.flushLogs(...args); }, + + // ------------------------------------------------------------------------- + // Canonical SDK events — flat aliases with Swift names delegating to the + // `RunAnywhere.sdkEvents.*` adapter. Mirrors Swift's flat subscribeSDKEvents + // / publishSDKEvent / pollSDKEvent / publishSDKFailure + // (RunAnywhere+SDKEvents.swift). Unsubscribe is the closure returned by + // subscribe (Web has no id-based unsubscribe), matching React Native. + // ------------------------------------------------------------------------- + + subscribeSDKEvents( + ...args: Parameters + ): ReturnType { + return SDKEvents.subscribe(...args); + }, + + publishSDKEvent( + ...args: Parameters + ): ReturnType { + return SDKEvents.publish(...args); + }, + + pollSDKEvent( + ...args: Parameters + ): ReturnType { + return SDKEvents.poll(...args); + }, + + publishSDKFailure( + ...args: Parameters + ): ReturnType { + return SDKEvents.publishFailure(...args); + }, }; diff --git a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+ModelRegistry.ts b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+ModelRegistry.ts index a9240dd17a..a162fc8c61 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+ModelRegistry.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+ModelRegistry.ts @@ -7,7 +7,8 @@ import type { ModelCategory} from '@runanywhere/proto-ts/model_types'; import { - InferenceFramework + InferenceFramework, + ModelFileRole, } from '@runanywhere/proto-ts/model_types'; import { ModelRegistryAdapter, @@ -18,6 +19,10 @@ import { getModuleForCapability, type EmscriptenRunanywhereModule, } from '../../runtime/EmscriptenModule.js'; +import { ProtoWasmBridge } from '../../runtime/ProtoWasm.js'; +import { SDKLogger } from '../../Foundation/SDKLogger.js'; + +const logger = new SDKLogger('ModelRegistry'); interface DefaultFrameworkModule extends EmscriptenRunanywhereModule { /** Proto-int wrapper over rac_model_category_default_framework (wasm_exports.cpp). */ @@ -107,4 +112,26 @@ export const ModelRegistry = { ? protoFramework : InferenceFramework.INFERENCE_FRAMEWORK_UNKNOWN) as InferenceFramework; }, + + /** + * Classify a sidecar filename's descriptor role for a given modality. + * Routed through the shared commons classifier + * (`rac_wasm_infer_model_file_role`) so the heuristic stays byte-identical + * with every other SDK. Mirrors Swift + * `RunAnywhere.inferModelFileRole(filename:modality:)`. Returns + * `MODEL_FILE_ROLE_PRIMARY_MODEL` when the WASM export is unavailable. + */ + inferModelFileRole(filename: string, modality: ModelCategory): ModelFileRole { + const module = getModuleForCapability('commons'); + if (!module) { + return ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL; + } + const role = new ProtoWasmBridge(module, logger).inferModelFileRole( + filename, + modality, + ); + return (role in ModelFileRole + ? role + : ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL) as ModelFileRole; + }, }; diff --git a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts index d674577cf9..33ecd0cee2 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts @@ -193,6 +193,25 @@ export interface SynthesizeOptions extends Partial { export const TTS = { synthesizeAuto: synthesize, + /** + * Streaming synthesis on the lifecycle-loaded TTS model (no component + * handle). Handle-less form backing Swift's `RunAnywhere.synthesizeStream(_: + * options:)`. The handle-owning form stays on `RunAnywhere.tts.synthesizeStream`. + */ + synthesizeStreamAuto( + text: string, + options?: Partial, + ): AsyncIterable { + const adapter = TTSProtoAdapter.tryDefault(); + if (!adapter || !adapter.supportsProtoTTS()) { + throw SDKException.backendNotAvailable( + 'TTS.synthesizeStreamAuto', + 'No Web WASM backend with rac_tts_*_proto exports is registered.', + ); + } + return adapter.synthesizeLifecycleStream(text, defaultTTSOptions(options)); + }, + /** * Returns true when the WASM module is loaded with both the proto-byte * TTS exports AND the component lifecycle exports (create / load_voice / diff --git a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+VAD.ts b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+VAD.ts index 6af361f8f9..62bb9c5ffd 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+VAD.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+VAD.ts @@ -229,6 +229,19 @@ export const VAD = { detectVoiceAuto: detectVoice, streamVoiceAuto: streamVoiceActivity, + /** + * Reset the lifecycle-loaded VAD service (clears speech-segment buffers). + * Handle-less form backing Swift's parameterless `RunAnywhere.resetVAD()`. + * No-op returning false when no VAD model is loaded through lifecycle. + */ + resetVoiceAuto(): boolean { + if (!currentLifecycleVADModel()) return false; + return ( + lifecycleVADAdapter('RunAnywhere.vad.resetVoiceAuto').resetLifecycle() != + null + ); + }, + /** * Returns true when the WASM module is loaded with both the proto-byte VAD * exports AND the component lifecycle exports (create / destroy). diff --git a/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts index ea19bf3626..7607b42d44 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts @@ -1879,15 +1879,17 @@ export const RunAnywhere = { }, synthesizeStream( - handle: Parameters[0], - text: Parameters[1], - options: Parameters[2], + text: Parameters[0], + options?: Parameters[1], extra: CancellableCall = {}, - ): ReturnType { + ): ReturnType { + // Swift-shaped lifecycle-owned stream: mirrors + // `RunAnywhere.synthesizeStream(_:options:)` (no component handle). The + // handle-owning form stays on `RunAnywhere.tts.synthesizeStream(handle,...)`. throwIfAborted(extra.signal, 'synthesizeStream'); - const iterable = TTSCapability.synthesizeStream(handle, text, options); + const iterable = TTSCapability.synthesizeStreamAuto(text, options); if (!extra.signal) return iterable; - const detach = attachSignalToCancel(extra.signal, () => TTSCapability.stop(handle)); + const detach = attachSignalToCancel(extra.signal, () => TTSCapability.stopLoaded()); return (async function* () { try { yield* iterable; From bc60e73050265a71b6d23176f36c769cdf833776 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sat, 18 Jul 2026 18:57:07 +0530 Subject: [PATCH 02/44] Fill missing telemetry fields (prompt_eval, embeddings tokens, lora) and add rcli rag/lora commands --- CMakePresets.json | 4 +- engines/llamacpp/llamacpp_backend.cpp | 13 +- engines/llamacpp/llamacpp_backend.h | 4 +- engines/llamacpp/rac_llm_llamacpp.cpp | 1 + engines/llamacpp/rac_vlm_llamacpp.cpp | 1 + engines/onnx/onnx_embedding_provider.cpp | 39 +- engines/onnx/onnx_embedding_provider.h | 7 +- engines/onnx/rac_onnx_embeddings_register.cpp | 10 +- idl/sdk_events.proto | 1 + sdk/runanywhere-cli/CMakeLists.txt | 1 + sdk/runanywhere-cli/src/app.cpp | 1 + sdk/runanywhere-cli/src/bootstrap.cpp | 265 +++- sdk/runanywhere-cli/src/commands/cmd_lora.cpp | 96 ++ sdk/runanywhere-cli/src/commands/cmd_rag.cpp | 221 +++ sdk/runanywhere-cli/src/commands/commands.h | 1 + .../docs/architecture.html | 224 +++ .../include/rac/features/llm/rac_llm_types.h | 3 + .../include/rac/features/vlm/rac_vlm_types.h | 3 + .../telemetry/rac_telemetry_types.h | 4 +- .../features/embeddings/embeddings_module.cpp | 18 +- .../src/features/llm/llm_module.cpp | 24 +- .../src/features/lora/rac_lora_service.cpp | 60 +- .../src/features/vlm/vlm_module.cpp | 8 +- .../src/generated/proto/sdk_events.pb.cc | 1212 +++++++++-------- .../src/generated/proto/sdk_events.pb.h | 42 +- .../telemetry/telemetry_json.cpp | 33 +- .../telemetry/telemetry_manager.cpp | 18 +- .../lib/generated/sdk_events.pb.dart | 12 + .../runanywhere/proto/v1/GenerationEvent.kt | 29 +- .../RunAnywhere/Generated/sdk_events.pb.swift | 15 +- sdk/shared/proto-ts/src/sdk_events.ts | 23 + 31 files changed, 1735 insertions(+), 658 deletions(-) create mode 100644 sdk/runanywhere-cli/src/commands/cmd_rag.cpp create mode 100644 sdk/runanywhere-commons/docs/architecture.html diff --git a/CMakePresets.json b/CMakePresets.json index 340aac9b97..983bd624ac 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -100,7 +100,7 @@ "RAC_BACKEND_COREML": "ON", "RAC_RUNTIME_ONNXRT": "ON", "RAC_RUNTIME_COREML": "ON", - "RAC_BACKEND_RAG": "OFF", + "RAC_BACKEND_RAG": "ON", "RAC_BUILD_SERVER": "ON", "RAC_STATIC_PLUGINS": "ON", "RAC_BUILD_SHARED": "OFF", @@ -127,7 +127,7 @@ "RAC_BACKEND_SHERPA": "ON", "RAC_BACKEND_ONNX": "ON", "RAC_RUNTIME_ONNXRT": "ON", - "RAC_BACKEND_RAG": "OFF", + "RAC_BACKEND_RAG": "ON", "RAC_BUILD_SERVER": "ON", "RAC_STATIC_PLUGINS": "ON", "RAC_BUILD_SHARED": "OFF", diff --git a/engines/llamacpp/llamacpp_backend.cpp b/engines/llamacpp/llamacpp_backend.cpp index bb903a10bd..34ae2c1403 100644 --- a/engines/llamacpp/llamacpp_backend.cpp +++ b/engines/llamacpp/llamacpp_backend.cpp @@ -820,6 +820,7 @@ TextGenerationResult LlamaCppTextGeneration::generate(const TextGenerationReques std::string generated_text; int tokens_generated = 0; int prompt_tokens = 0; + double prompt_eval_ms = 0.0; auto start_time = std::chrono::high_resolution_clock::now(); @@ -831,7 +832,7 @@ TextGenerationResult LlamaCppTextGeneration::generate(const TextGenerationReques tokens_generated++; return !cancel_requested_.load(); }, - &prompt_tokens); + &prompt_tokens, &prompt_eval_ms); RAC_LOG_INFO("LLM.LlamaCpp", "generate(): generate_stream returned success=%d, tokens=%d", success, tokens_generated); @@ -842,6 +843,7 @@ TextGenerationResult LlamaCppTextGeneration::generate(const TextGenerationReques result.tokens_generated = tokens_generated; result.prompt_tokens = prompt_tokens; result.inference_time_ms = duration.count(); + result.prompt_eval_time_ms = prompt_eval_ms; if (decode_failed_) { result.finish_reason = "error"; @@ -972,7 +974,8 @@ int LlamaCppTextGeneration::run_decode_loop(llama_sampler* sampler, llama_batch& } bool LlamaCppTextGeneration::generate_stream(const TextGenerationRequest& request, - TextStreamCallback callback, int* out_prompt_tokens) { + TextStreamCallback callback, int* out_prompt_tokens, + double* out_prompt_eval_ms) { std::lock_guard lock(mutex_); if (!is_ready_locked()) { @@ -1083,6 +1086,7 @@ bool LlamaCppTextGeneration::generate_stream(const TextGenerationRequest& reques prompt_tokens, n_batch); llama_batch batch = llama_batch_init(n_batch, 0, 1); + const auto prefill_start = std::chrono::steady_clock::now(); for (int chunk_start = 0; chunk_start < prompt_tokens; chunk_start += n_batch) { batch.n_tokens = 0; int chunk_end = std::min(chunk_start + n_batch, prompt_tokens); @@ -1100,6 +1104,11 @@ bool LlamaCppTextGeneration::generate_stream(const TextGenerationRequest& reques return false; } } + if (out_prompt_eval_ms) { + *out_prompt_eval_ms = std::chrono::duration( + std::chrono::steady_clock::now() - prefill_start) + .count(); + } RAC_LOG_INFO("LLM.LlamaCpp", "generate_stream: prompt decoded successfully"); // Configure sampler with request parameters — skip rebuild if params diff --git a/engines/llamacpp/llamacpp_backend.h b/engines/llamacpp/llamacpp_backend.h index f172ce3e14..cac50fb12d 100644 --- a/engines/llamacpp/llamacpp_backend.h +++ b/engines/llamacpp/llamacpp_backend.h @@ -54,6 +54,7 @@ struct TextGenerationResult { int tokens_generated = 0; int prompt_tokens = 0; double inference_time_ms = 0.0; + double prompt_eval_time_ms = 0.0; // prefill (prompt decode) wall-clock std::string finish_reason; // "stop", "length", "cancelled" }; @@ -152,9 +153,10 @@ class LlamaCppTextGeneration { * @param request Generation request. * @param callback Streaming callback; return false to cancel. * @param out_prompt_tokens Optional: tokenized prompt length (may be NULL). + * @param out_prompt_eval_ms Optional: prefill (prompt decode) time in ms (may be NULL). */ bool generate_stream(const TextGenerationRequest& request, TextStreamCallback callback, - int* out_prompt_tokens = nullptr); + int* out_prompt_tokens = nullptr, double* out_prompt_eval_ms = nullptr); void cancel(); diff --git a/engines/llamacpp/rac_llm_llamacpp.cpp b/engines/llamacpp/rac_llm_llamacpp.cpp index df31673cbf..ce570e7081 100644 --- a/engines/llamacpp/rac_llm_llamacpp.cpp +++ b/engines/llamacpp/rac_llm_llamacpp.cpp @@ -340,6 +340,7 @@ rac_result_t rac_llm_llamacpp_generate(rac_handle_t handle, const char* prompt, out_result->prompt_tokens = result.prompt_tokens; out_result->total_tokens = result.prompt_tokens + result.tokens_generated; out_result->time_to_first_token_ms = 0; + out_result->prompt_eval_time_ms = static_cast(result.prompt_eval_time_ms); out_result->total_time_ms = result.inference_time_ms; out_result->tokens_per_second = result.tokens_generated > 0 && result.inference_time_ms > 0 diff --git a/engines/llamacpp/rac_vlm_llamacpp.cpp b/engines/llamacpp/rac_vlm_llamacpp.cpp index 82430b2916..efe3ddfe2e 100644 --- a/engines/llamacpp/rac_vlm_llamacpp.cpp +++ b/engines/llamacpp/rac_vlm_llamacpp.cpp @@ -1468,6 +1468,7 @@ rac_result_t rac_vlm_llamacpp_process(rac_handle_t handle, const rac_vlm_image_t const double decode_ms = ms(t_end - t_after_prep).count(); out_result->total_time_ms = static_cast(total_ms); out_result->image_encode_time_ms = static_cast(ms(t_after_prep - t_start).count()); + out_result->prompt_eval_time_ms = static_cast(ms(t_after_prep - t_start).count()); out_result->time_to_first_token_ms = static_cast(ms(t_first_token - t_start).count()); out_result->tokens_per_second = decode_ms > 0.0 ? static_cast(tokens_generated / (decode_ms / 1000.0)) : 0.0f; diff --git a/engines/onnx/onnx_embedding_provider.cpp b/engines/onnx/onnx_embedding_provider.cpp index 6b371b92e4..0082192558 100644 --- a/engines/onnx/onnx_embedding_provider.cpp +++ b/engines/onnx/onnx_embedding_provider.cpp @@ -550,7 +550,7 @@ class ONNXEmbeddingProvider::Impl { ~Impl() = default; - std::vector embed(const std::string& text) { + std::vector embed(const std::string& text, size_t* out_total_tokens = nullptr) { if (!ready_) { LOGE("Embedding provider not ready"); return {}; @@ -560,6 +560,10 @@ class ONNXEmbeddingProvider::Impl { try { auto token_ids = tokenizer_.encode_unpadded(text, max_seq_length_); + const size_t real_tokens = token_ids.size(); + if (out_total_tokens) { + *out_total_tokens = real_tokens; + } const size_t pad_length = align_up(token_ids.size(), 8); tokenizer_.pad_to(token_ids, pad_length); @@ -630,14 +634,18 @@ class ONNXEmbeddingProvider::Impl { } } - std::vector> embed_batch(const std::vector& texts) { + std::vector> embed_batch(const std::vector& texts, + size_t* out_total_tokens = nullptr) { + if (out_total_tokens) { + *out_total_tokens = 0; + } if (texts.empty()) { return {}; } // Delegate to single embed for batch_size == 1 if (texts.size() == 1) { - return {embed(texts[0])}; + return {embed(texts[0], out_total_tokens)}; } if (!ready_) { @@ -649,6 +657,7 @@ class ONNXEmbeddingProvider::Impl { std::vector> all_results; all_results.reserve(texts.size()); + size_t total_tokens = 0; for (size_t offset = 0; offset < texts.size(); offset += kMaxSubBatchSize) { size_t sub_batch_size = std::min(kMaxSubBatchSize, texts.size() - offset); @@ -656,17 +665,23 @@ class ONNXEmbeddingProvider::Impl { LOGI("Embedding sub-batch %zu/%zu (size=%zu)", offset / kMaxSubBatchSize + 1, (texts.size() + kMaxSubBatchSize - 1) / kMaxSubBatchSize, sub_batch_size); - auto sub_results = embed_sub_batch(texts, offset, sub_batch_size); + size_t sub_tokens = 0; + auto sub_results = embed_sub_batch(texts, offset, sub_batch_size, &sub_tokens); if (sub_results.empty()) { LOGE("Sub-batch embedding failed at offset %zu", offset); return {}; } + total_tokens += sub_tokens; for (auto& r : sub_results) { all_results.push_back(std::move(r)); } } + if (out_total_tokens) { + *out_total_tokens = total_tokens; + } + LOGI("Generated batch embeddings: count=%zu, dim=%zu", all_results.size(), embedding_dim_); return all_results; } @@ -684,14 +699,20 @@ class ONNXEmbeddingProvider::Impl { } std::vector> embed_sub_batch(const std::vector& texts, - size_t offset, size_t count) { + size_t offset, size_t count, + size_t* out_total_tokens = nullptr) { try { std::vector> all_token_ids(count); size_t max_actual_len = 0; + size_t total_tokens = 0; for (size_t i = 0; i < count; ++i) { all_token_ids[i] = tokenizer_.encode_unpadded(texts[offset + i], max_seq_length_); max_actual_len = std::max(max_actual_len, all_token_ids[i].size()); + total_tokens += all_token_ids[i].size(); + } + if (out_total_tokens) { + *out_total_tokens = total_tokens; } const size_t pad_length = align_up(max_actual_len, 8); @@ -863,13 +884,13 @@ ONNXEmbeddingProvider::~ONNXEmbeddingProvider() = default; ONNXEmbeddingProvider::ONNXEmbeddingProvider(ONNXEmbeddingProvider&&) noexcept = default; ONNXEmbeddingProvider& ONNXEmbeddingProvider::operator=(ONNXEmbeddingProvider&&) noexcept = default; -std::vector ONNXEmbeddingProvider::embed(const std::string& text) { - return impl_->embed(text); +std::vector ONNXEmbeddingProvider::embed(const std::string& text, size_t* out_total_tokens) { + return impl_->embed(text, out_total_tokens); } std::vector> -ONNXEmbeddingProvider::embed_batch(const std::vector& texts) { - return impl_->embed_batch(texts); +ONNXEmbeddingProvider::embed_batch(const std::vector& texts, size_t* out_total_tokens) { + return impl_->embed_batch(texts, out_total_tokens); } size_t ONNXEmbeddingProvider::dimension() const noexcept { diff --git a/engines/onnx/onnx_embedding_provider.h b/engines/onnx/onnx_embedding_provider.h index d0488b1301..1697583a62 100644 --- a/engines/onnx/onnx_embedding_provider.h +++ b/engines/onnx/onnx_embedding_provider.h @@ -36,8 +36,11 @@ class ONNXEmbeddingProvider { ONNXEmbeddingProvider(ONNXEmbeddingProvider&&) noexcept; ONNXEmbeddingProvider& operator=(ONNXEmbeddingProvider&&) noexcept; - std::vector embed(const std::string& text); - std::vector> embed_batch(const std::vector& texts); + // out_total_tokens (optional): receives the real, non-padding token count + // consumed across the input(s). Nullptr-safe; existing callers are unaffected. + std::vector embed(const std::string& text, size_t* out_total_tokens = nullptr); + std::vector> embed_batch(const std::vector& texts, + size_t* out_total_tokens = nullptr); size_t dimension() const noexcept; bool is_ready() const noexcept; const char* name() const noexcept; diff --git a/engines/onnx/rac_onnx_embeddings_register.cpp b/engines/onnx/rac_onnx_embeddings_register.cpp index 2b95075a6d..303a5daac6 100644 --- a/engines/onnx/rac_onnx_embeddings_register.cpp +++ b/engines/onnx/rac_onnx_embeddings_register.cpp @@ -56,7 +56,8 @@ static rac_result_t onnx_embed_vtable_embed(void* impl, const char* text, return RAC_ERROR_BACKEND_NOT_READY; try { - auto embedding = h->provider->embed(text); + size_t total_tokens = 0; + auto embedding = h->provider->embed(text, &total_tokens); // The provider uses an empty vector as its failure sentinel // (onnx_embedding_provider.cpp:591-633 — model run / dtype mismatch / // exception all return {}). Treat that as RAC_ERROR_INFERENCE_FAILED so @@ -71,7 +72,7 @@ static rac_result_t onnx_embed_vtable_embed(void* impl, const char* text, out_result->num_embeddings = 1; out_result->dimension = dim; out_result->processing_time_ms = 0; - out_result->total_tokens = 0; + out_result->total_tokens = static_cast(total_tokens); out_result->embeddings = static_cast(malloc(sizeof(rac_embedding_vector_t))); @@ -113,7 +114,8 @@ static rac_result_t onnx_embed_vtable_embed_batch(void* impl, const char* const* texts_vec.emplace_back(texts[i]); } - auto batch_results = h->provider->embed_batch(texts_vec); + size_t total_tokens = 0; + auto batch_results = h->provider->embed_batch(texts_vec, &total_tokens); if (batch_results.size() != num_texts) { RAC_LOG_ERROR(LOG_CAT, "Batch embedding returned %zu results, expected %zu", batch_results.size(), num_texts); @@ -134,7 +136,7 @@ static rac_result_t onnx_embed_vtable_embed_batch(void* impl, const char* const* out_result->num_embeddings = num_texts; out_result->dimension = dim; out_result->processing_time_ms = 0; - out_result->total_tokens = 0; + out_result->total_tokens = static_cast(total_tokens); if (num_texts == 0) { out_result->embeddings = nullptr; diff --git a/idl/sdk_events.proto b/idl/sdk_events.proto index e5408d547e..779cc7800c 100644 --- a/idl/sdk_events.proto +++ b/idl/sdk_events.proto @@ -433,6 +433,7 @@ message GenerationEvent { string model_name = 31; double duration_ms = 32; // wall-clock generation duration int32 framework = 33; // InferenceFramework enum int + int64 prompt_eval_time_ms = 34; // prompt eval (prefill) duration } enum GenerationEventKind { diff --git a/sdk/runanywhere-cli/CMakeLists.txt b/sdk/runanywhere-cli/CMakeLists.txt index af2171f06c..db230290c0 100644 --- a/sdk/runanywhere-cli/CMakeLists.txt +++ b/sdk/runanywhere-cli/CMakeLists.txt @@ -38,6 +38,7 @@ set(RCLI_SOURCES src/commands/cmd_tts.cpp src/commands/cmd_vad.cpp src/commands/cmd_voice.cpp + src/commands/cmd_rag.cpp src/commands/engine_options.cpp src/commands/model_setup.cpp src/config/cli_paths.cpp diff --git a/sdk/runanywhere-cli/src/app.cpp b/sdk/runanywhere-cli/src/app.cpp index f01ea67a26..4852ba89df 100644 --- a/sdk/runanywhere-cli/src/app.cpp +++ b/sdk/runanywhere-cli/src/app.cpp @@ -43,6 +43,7 @@ void configure_app(CLI::App& app, GlobalOptions& options) { commands::register_tts(app, options); commands::register_vad(app, options); commands::register_voice(app, options); + commands::register_rag(app, options); commands::register_serve(app, options); } diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index 3edbfb1221..40c478cb69 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #if !defined(_WIN32) #include #endif @@ -14,6 +16,17 @@ #include "rac/infrastructure/device/rac_device_identity.h" #include "rac/infrastructure/model_management/rac_model_paths.h" #include "rac/infrastructure/network/rac_environment.h" +#include "rac/infrastructure/network/rac_auth_manager.h" +#include "rac/infrastructure/network/rac_endpoints.h" +#include "rac/infrastructure/http/rac_http_client.h" +#include "rac/infrastructure/http/rac_http_transport.h" +#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" +#include "rac/infrastructure/events/rac_sdk_event_stream.h" +#include "rac/core/rac_sdk_state.h" +#include "rac/lifecycle/rac_sdk_init.h" +#include "rac/foundation/rac_proto_buffer.h" + +#include "sdk_init.pb.h" #include "catalog/catalog.h" #include "config/cli_paths.h" @@ -48,6 +61,10 @@ namespace { rac_platform_adapter_t g_adapter{}; bool g_bootstrapped = false; +// Owns the telemetry manager for the process lifetime so the terminal flush in +// rac_shutdown() can deliver through our HTTP callback before teardown. +rac_telemetry_manager_t *g_telemetry_manager = nullptr; + rac_log_level_t log_level_for(const GlobalOptions &options) { if (options.verbose) { return RAC_LOG_DEBUG; @@ -150,6 +167,23 @@ const char *desktop_platform() { #endif } +rac_environment_t environment_from_name(const std::string &name) { + if (name == "production" || name == "prod") + return RAC_ENV_PRODUCTION; + if (name == "staging") + return RAC_ENV_STAGING; + return RAC_ENV_DEVELOPMENT; +} + +::runanywhere::v1::SdkInitEnvironment +proto_environment_from_name(const std::string &name) { + if (name == "production" || name == "prod") + return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_PRODUCTION; + if (name == "staging") + return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_STAGING; + return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_DEVELOPMENT; +} + void initialize_sdk_metadata() { char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; const rac_result_t device_rc = @@ -163,10 +197,17 @@ void initialize_sdk_metadata() { const std::string locale = detect_locale(); const std::string timezone = detect_timezone(); + const std::string api_key = + first_env_value("RUNANYWHERE_API_KEY", nullptr, nullptr); + const std::string base_url = + first_env_value("RUNANYWHERE_BASE_URL", nullptr, nullptr); + const std::string environment_name = + first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); + rac_sdk_config_t sdk_config = {}; - sdk_config.environment = RAC_ENV_DEVELOPMENT; - sdk_config.api_key = ""; - sdk_config.base_url = ""; + sdk_config.environment = environment_from_name(environment_name); + sdk_config.api_key = api_key.c_str(); + sdk_config.base_url = base_url.c_str(); sdk_config.device_id = device_id[0] != '\0' ? device_id : ""; sdk_config.platform = desktop_platform(); sdk_config.sdk_version = RCLI_VERSION; @@ -185,6 +226,216 @@ void initialize_sdk_metadata() { } } +// Delivers a queued telemetry batch over the desktop HTTP transport. Wired via +// rac_telemetry_manager_set_http_callback (user_data = the manager) so the +// outcome is reported back through rac_telemetry_manager_http_complete. Mirrors +// the control-plane POST performed by commons' auth path. +void rcli_telemetry_http_callback(void *user_data, const char *endpoint, + const char *json_body, size_t json_length, + rac_bool_t requires_auth) { + auto *manager = static_cast(user_data); + const char *base_url = rac_state_get_base_url(); + if (base_url == nullptr || base_url[0] == '\0' || + rac_http_transport_is_registered() != RAC_TRUE) { + if (manager != nullptr) { + rac_telemetry_manager_http_complete(manager, RAC_FALSE, nullptr, + "telemetry transport unavailable"); + } + return; + } + + char url[2048] = {}; + if (rac_build_url(base_url, endpoint, url, sizeof(url)) < 0) { + if (manager != nullptr) { + rac_telemetry_manager_http_complete(manager, RAC_FALSE, nullptr, + "telemetry URL build failed"); + } + return; + } + + std::vector headers; + const rac_http_header_kv_t *defaults = nullptr; + size_t default_count = 0; + if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && + defaults != nullptr) { + headers.assign(defaults, defaults + default_count); + } + std::string auth_value; + if (requires_auth == RAC_TRUE) { + const char *token = rac_auth_get_access_token(); + if (token != nullptr && token[0] != '\0') { + auth_value = std::string("Bearer ") + token; + headers.push_back({"Authorization", auth_value.c_str()}); + } + } + + rac_http_client_t *client = nullptr; + if (rac_http_client_create(&client) != RAC_SUCCESS) { + if (manager != nullptr) { + rac_telemetry_manager_http_complete(manager, RAC_FALSE, nullptr, + "telemetry client create failed"); + } + return; + } + + rac_http_request_t request = {}; + request.method = "POST"; + request.url = url; + request.headers = headers.empty() ? nullptr : headers.data(); + request.header_count = headers.size(); + request.body_bytes = reinterpret_cast(json_body); + request.body_len = json_length; + request.timeout_ms = rac_env_default_http_timeout_ms(rac_state_get_environment()); + request.follow_redirects = RAC_FALSE; + + rac_http_response_t response = {}; + const rac_result_t rc = rac_http_request_send(client, &request, &response); + rac_http_client_destroy(client); + + const bool ok = + rc == RAC_SUCCESS && response.status >= 200 && response.status < 300; + std::string body; + if (response.body_bytes != nullptr && response.body_len > 0) { + body.assign(reinterpret_cast(response.body_bytes), + response.body_len); + } + if (!ok) { + // Surface the exact backend rejection (status + response body) so schema + // mismatches (e.g. strict extra_forbidden 422s) are diagnosable from rcli. + out::status_line(std::string("telemetry POST ") + (endpoint ? endpoint : "?") + + " -> rc=" + out::describe_result(rc) + + " http=" + std::to_string(response.status) + + " body=" + (body.empty() ? "(empty)" : body)); + // DEBUG: dump the exact request JSON so a malformed offset can be inspected. + if (const char *dump = std::getenv("RCLI_TELEMETRY_DUMP"); + dump != nullptr && dump[0] != '\0' && json_body != nullptr) { + if (FILE *fp = std::fopen(dump, "ab")) { + std::fwrite(json_body, 1, json_length, fp); + std::fputc('\n', fp); + std::fclose(fp); + } + } + } + if (manager != nullptr) { + rac_telemetry_manager_http_complete(manager, ok ? RAC_TRUE : RAC_FALSE, + body.empty() ? nullptr : body.c_str(), + ok ? nullptr : "telemetry POST failed"); + } + rac_http_response_free(&response); +} + +// Runs the canonical two-phase SDK init so rcli authenticates and telemetry +// actually flushes. Phase 1 sets environment + credentials; Phase 2 +// authenticates, registers the device, and enables the telemetry sink. +// Credentials come from the environment (RUNANYWHERE_API_KEY / +// RUNANYWHERE_BASE_URL / RUNANYWHERE_ENVIRONMENT) so no secrets live in source. +// When credentials are absent, rcli stays in local dev mode (no auth, no +// telemetry) exactly as before. +void initialize_telemetry_auth() { + const std::string api_key = + first_env_value("RUNANYWHERE_API_KEY", nullptr, nullptr); + const std::string base_url = + first_env_value("RUNANYWHERE_BASE_URL", nullptr, nullptr); + const std::string environment_name = + first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); + + if (api_key.empty() || base_url.empty()) { + return; // Local dev mode — telemetry not sent (staging/prod only). + } + + // Enable the auth manager. NULL secure storage: tokens are not persisted + // across runs (fine for a CLI session); authentication still runs per run. + rac_auth_init(nullptr); + + char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; + if (rac_device_get_or_create_persistent_id(device_id, sizeof(device_id)) != + RAC_SUCCESS) { + device_id[0] = '\0'; + } + + // Create + register the telemetry sink BEFORE Phase 2 so its flush has a sink + // and events emitted during subsequent commands are tracked. Delivery runs + // through rcli_telemetry_http_callback over the desktop HTTP transport; the + // terminal batch flushes in rac_shutdown() during teardown. + g_telemetry_manager = rac_telemetry_manager_create( + environment_from_name(environment_name), + device_id[0] != '\0' ? device_id : "", desktop_platform(), RCLI_VERSION); + if (g_telemetry_manager != nullptr) { + rac_telemetry_manager_set_http_callback( + g_telemetry_manager, rcli_telemetry_http_callback, g_telemetry_manager); + rac_events_set_telemetry_sink(g_telemetry_manager); + } + + ::runanywhere::v1::SdkInitPhase1Request phase1; + phase1.set_environment(proto_environment_from_name(environment_name)); + phase1.set_api_key(api_key); + phase1.set_base_url(base_url); + if (device_id[0] != '\0') { + phase1.set_device_id(device_id); + } + phase1.set_platform(desktop_platform()); + phase1.set_sdk_version(RCLI_VERSION); + + std::string phase1_bytes; + if (!phase1.SerializeToString(&phase1_bytes)) { + out::status_line("warning: telemetry phase 1 serialize failed"); + return; + } + + rac_proto_buffer_t phase1_out; + rac_proto_buffer_init(&phase1_out); + rac_result_t rc = rac_sdk_init_phase1_proto( + reinterpret_cast(phase1_bytes.data()), + phase1_bytes.size(), &phase1_out); + rac_proto_buffer_free(&phase1_out); + if (rc != RAC_SUCCESS) { + out::status_line("warning: telemetry phase 1 failed: " + + out::describe_result(rc)); + return; + } + + ::runanywhere::v1::SdkInitPhase2Request phase2; + phase2.set_flush_telemetry(true); + phase2.set_discover_downloaded_models(true); + phase2.set_rescan_local_models(true); + + std::string phase2_bytes; + if (!phase2.SerializeToString(&phase2_bytes)) { + out::status_line("warning: telemetry phase 2 serialize failed"); + return; + } + + rac_proto_buffer_t phase2_out; + rac_proto_buffer_init(&phase2_out); + rc = rac_sdk_init_phase2_proto( + reinterpret_cast(phase2_bytes.data()), + phase2_bytes.size(), &phase2_out); + + ::runanywhere::v1::SdkInitResult result; + const bool parsed = phase2_out.status == RAC_SUCCESS && + phase2_out.data != nullptr && + result.ParseFromArray(phase2_out.data, + static_cast(phase2_out.size)); + rac_proto_buffer_free(&phase2_out); + + if (rc != RAC_SUCCESS) { + out::status_line("warning: telemetry phase 2 failed: " + + out::describe_result(rc)); + return; + } + + if (parsed) { + std::string note = std::string("telemetry ready | http_configured=") + + (result.http_configured() ? "yes" : "no") + + " device_registered=" + + (result.device_registered() ? "yes" : "no"); + if (!result.warning().empty()) { + note += " | " + result.warning(); + } + out::status_line(note); + } +} + } // namespace rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { @@ -235,6 +486,7 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { } initialize_sdk_metadata(); + initialize_telemetry_auth(); #if defined(RCLI_HAS_LLAMACPP) if (rac_backend_llamacpp_register() != RAC_SUCCESS) { @@ -288,7 +540,14 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { void shutdown() { if (g_bootstrapped) { + // rac_shutdown() flushes the terminal telemetry batch through the + // registered sink (our HTTP callback) before clearing lifetime state. rac_shutdown(); + rac_events_set_telemetry_sink(nullptr); + if (g_telemetry_manager != nullptr) { + rac_telemetry_manager_destroy(g_telemetry_manager); + g_telemetry_manager = nullptr; + } g_bootstrapped = false; } } diff --git a/sdk/runanywhere-cli/src/commands/cmd_lora.cpp b/sdk/runanywhere-cli/src/commands/cmd_lora.cpp index 3af0f2d5c4..69877eae51 100644 --- a/sdk/runanywhere-cli/src/commands/cmd_lora.cpp +++ b/sdk/runanywhere-cli/src/commands/cmd_lora.cpp @@ -15,7 +15,9 @@ #include #include "lora_options.pb.h" +#include "model_types.pb.h" #include "rac/core/rac_core.h" +#include "rac/core/rac_model_lifecycle.h" #include "rac/features/lora/rac_lora_service.h" #include "io/output.h" @@ -146,6 +148,82 @@ int run_lora_list(const GlobalOptions &options) { return 0; } +// Load an LLM through the model-lifecycle service so rac_lora_apply_proto can +// acquire it. validate_availability=true auto-pulls the model if missing. +bool load_llm_for_lora(const GlobalOptions &options, const std::string &model_id) { + v1::ModelLoadRequest request; + request.set_model_id(model_id); + request.set_category(v1::MODEL_CATEGORY_LANGUAGE); + request.set_validate_availability(true); + + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t out_buffer; + rac_proto_buffer_init(&out_buffer); + std::string error; + v1::ModelLoadResult result; + if (rac_model_lifecycle_load_proto(rac_get_model_registry(), + reinterpret_cast(bytes.data()), bytes.size(), + &out_buffer) != RAC_SUCCESS || + !proto::parse_proto_buffer(&out_buffer, &result, &error)) { + out::error_line("LLM load failed: " + error); + return false; + } + if (!result.success()) { + out::error_line("LLM load failed: " + + (result.error_message().empty() ? "unknown error" : result.error_message())); + return false; + } + return true; +} + +int run_lora_apply(const GlobalOptions &options, const std::string &model_id, + const std::string &adapter_path, float scale) { + Bootstrapped env; + if (bootstrap(options, &env) != RAC_SUCCESS) { + return 1; + } + if (!load_llm_for_lora(options, model_id)) { + return 1; + } + + v1::LoRAApplyRequest request; + request.set_replace_existing(true); + v1::LoRAAdapterConfig *adapter = request.add_adapters(); + adapter->set_adapter_path(adapter_path); + adapter->set_scale(scale); + + const std::string request_bytes = proto::serialize(request); + rac_proto_buffer_t out_buffer; + rac_proto_buffer_init(&out_buffer); + const rac_result_t rc = rac_lora_apply_proto( + reinterpret_cast(request_bytes.data()), request_bytes.size(), &out_buffer); + v1::LoRAApplyResult result; + std::string error; + if (!proto::parse_proto_buffer(&out_buffer, &result, &error) || rc != RAC_SUCCESS) { + out::error_line("apply failed: " + error); + return 1; + } + if (!result.success()) { + out::error_line("apply failed: " + + (result.error_message().empty() ? std::to_string(result.error_code()) + : result.error_message())); + return 1; + } + + if (options.json) { + out::JsonWriter json; + json.begin_object() + .field("success", result.success()) + .field("adapters", static_cast(result.adapters_size())) + .end_object(); + out::result_line(json.str()); + } else { + out::result_line("applied " + std::to_string(result.adapters_size()) + " adapter(s) to " + + model_id); + } + return 0; +} + } // namespace void register_lora(CLI::App &app, GlobalOptions &options) { @@ -172,6 +250,24 @@ void register_lora(CLI::App &app, GlobalOptions &options) { throw CLI::RuntimeError(exit_code); } }); + + CLI::App *apply_cmd = cmd->add_subcommand( + "apply", "Load an LLM and attach a LoRA adapter (.gguf) to it"); + auto apply_model = std::make_shared(); + auto adapter_path = std::make_shared(); + auto scale = std::make_shared(1.0f); + apply_cmd->add_option("adapter", *adapter_path, "Path to the adapter file (.gguf)") + ->required(); + apply_cmd->add_option("--model,-m", *apply_model, "LLM model id to attach the adapter to") + ->required(); + apply_cmd->add_option("--scale", *scale, "Adapter scale factor (default: 1.0)") + ->default_val(1.0f); + apply_cmd->callback([&options, apply_model, adapter_path, scale]() { + const int exit_code = run_lora_apply(options, *apply_model, *adapter_path, *scale); + if (exit_code != 0) { + throw CLI::RuntimeError(exit_code); + } + }); } } // namespace rcli::commands diff --git a/sdk/runanywhere-cli/src/commands/cmd_rag.cpp b/sdk/runanywhere-cli/src/commands/cmd_rag.cpp new file mode 100644 index 0000000000..1648f2522f --- /dev/null +++ b/sdk/runanywhere-cli/src/commands/cmd_rag.cpp @@ -0,0 +1,221 @@ +/** + * @file cmd_rag.cpp + * @brief `rcli rag query` — retrieval-augmented generation via the commons + * RAG session ABI. + * + * Single-shot flow in one process (the CLI is stateless across invocations and + * RAG indexes are in-memory only): + * rac_rag_session_create_proto(RAGConfiguration) → session handle + * → rac_rag_ingest_proto(RAGDocument) per --doc / --file + * → rac_rag_query_proto(RAGQueryOptions) → RAGResult + * rac_rag_session_destroy_proto(session) + * + * Commons resolves the embedding + LLM model ids to filesystem paths via the + * model registry and owns the full embed → retrieve → generate pipeline; this + * command only translates argv to the rac_rag_* C ABI and renders the result. + */ + +#include "commands/commands.h" + +#include +#include +#include +#include +#include + +#include "rag.pb.h" +#include "rac/core/rac_core.h" +#include "rac/features/rag/rac_rag.h" + +#include "io/output.h" +#include "io/proto.h" + +namespace rcli::commands { + +namespace { + +namespace v1 = runanywhere::v1; + +constexpr const char* kDefaultRagLlm = "smollm2-360m-q8_0"; +constexpr const char* kDefaultRagEmbed = "all-minilm-l6-v2"; + +bool read_text_file(const std::string& path, std::string* out, std::string* error) { + std::ifstream file(path, std::ios::binary); + if (!file) { + *error = "cannot open file: " + path; + return false; + } + std::ostringstream buffer; + buffer << file.rdbuf(); + *out = buffer.str(); + return true; +} + +int run_rag_query(const GlobalOptions& options, const std::string& llm_model, + const std::string& embed_model, const std::vector& docs, + const std::vector& files, const std::string& question, + int top_k, int max_tokens, float temperature) { + Bootstrapped env; + if (bootstrap(options, &env) != RAC_SUCCESS) { + return 1; + } + + if (question.empty()) { + out::error_line("a question is required (positional argument)"); + return 2; + } + + std::vector documents = docs; + for (const auto& path : files) { + std::string content; + std::string error; + if (!read_text_file(path, &content, &error)) { + out::error_line(error); + return 2; + } + documents.push_back(content); + } + if (documents.empty()) { + out::error_line("at least one document is required (--doc or --file)"); + return 2; + } + + // Both models must already be downloaded — the session resolves them from + // the registry. (Pull them first with `rcli pull ` if missing.) + // Create the RAG session. + v1::RAGConfiguration config; + config.set_embedding_model_id(embed_model); + config.set_llm_model_id(llm_model); + if (top_k > 0) { + config.set_top_k(top_k); + } + + const std::string config_bytes = proto::serialize(config); + rac_handle_t session = nullptr; + if (rac_rag_session_create_proto(reinterpret_cast(config_bytes.data()), + config_bytes.size(), &session) != RAC_SUCCESS || + session == nullptr) { + out::error_line("RAG session create failed (check that '" + embed_model + "' and '" + + llm_model + "' are downloaded)"); + return 1; + } + + // Ingest each document. + std::string error; + for (size_t i = 0; i < documents.size(); ++i) { + v1::RAGDocument document; + document.set_id("doc-" + std::to_string(i)); + document.set_text(documents[i]); + const std::string doc_bytes = proto::serialize(document); + rac_proto_buffer_t stats_buffer; + rac_proto_buffer_init(&stats_buffer); + v1::RAGStatistics stats; + if (rac_rag_ingest_proto(session, reinterpret_cast(doc_bytes.data()), + doc_bytes.size(), &stats_buffer) != RAC_SUCCESS || + !proto::parse_proto_buffer(&stats_buffer, &stats, &error)) { + out::error_line("RAG ingest failed: " + error); + rac_rag_session_destroy_proto(session); + return 1; + } + if (options.verbose) { + out::status_line("ingested doc-" + std::to_string(i) + " (" + + std::to_string(documents[i].size()) + " bytes)"); + } + } + + // Query. + v1::RAGQueryOptions query; + query.set_question(question); + if (max_tokens > 0) { + query.set_max_tokens(max_tokens); + } + if (temperature >= 0.0f) { + query.set_temperature(temperature); + } + if (top_k > 0) { + query.set_retrieval_top_k(top_k); + } + + const std::string query_bytes = proto::serialize(query); + rac_proto_buffer_t result_buffer; + rac_proto_buffer_init(&result_buffer); + v1::RAGResult result; + if (rac_rag_query_proto(session, reinterpret_cast(query_bytes.data()), + query_bytes.size(), &result_buffer) != RAC_SUCCESS || + !proto::parse_proto_buffer(&result_buffer, &result, &error)) { + out::error_line("RAG query failed: " + error); + rac_rag_session_destroy_proto(session); + return 1; + } + + if (result.error_code() != 0 || result.has_error_message()) { + out::error_line("RAG query failed: " + (result.error_message().empty() + ? std::to_string(result.error_code()) + : result.error_message())); + rac_rag_session_destroy_proto(session); + return 1; + } + + if (options.json) { + out::JsonWriter json; + json.begin_object() + .field("answer", result.answer()) + .field("retrieved_chunks", static_cast(result.retrieved_chunks_size())) + .field("retrieval_time_ms", static_cast(result.retrieval_time_ms())) + .field("generation_time_ms", static_cast(result.generation_time_ms())) + .field("total_time_ms", static_cast(result.total_time_ms())) + .field("prompt_tokens", static_cast(result.prompt_tokens())) + .field("completion_tokens", static_cast(result.completion_tokens())); + out::result_line(json.end_object().str()); + } else { + out::result_line(result.answer()); + if (options.verbose) { + out::status_line("chunks=" + std::to_string(result.retrieved_chunks_size()) + + " retrieval=" + std::to_string(result.retrieval_time_ms()) + "ms" + + " generation=" + std::to_string(result.generation_time_ms()) + "ms"); + } + } + + rac_rag_session_destroy_proto(session); + return 0; +} + +} // namespace + +void register_rag(CLI::App& app, GlobalOptions& options) { + CLI::App* cmd = app.add_subcommand("rag", "Retrieval-augmented generation"); + CLI::App* query_cmd = cmd->add_subcommand("query", "Ingest documents and answer a question"); + + auto question = std::make_shared(); + auto docs = std::make_shared>(); + auto files = std::make_shared>(); + auto llm_model = std::make_shared(kDefaultRagLlm); + auto embed_model = std::make_shared(kDefaultRagEmbed); + auto top_k = std::make_shared(0); + auto max_tokens = std::make_shared(0); + auto temperature = std::make_shared(-1.0f); + + query_cmd->add_option("question", *question, "Question to answer over the ingested documents") + ->required(); + query_cmd->add_option("--doc,-d", *docs, "Inline document text (repeat for multiple)"); + query_cmd->add_option("--file,-f", *files, "Path to a text file to ingest (repeat for multiple)"); + query_cmd->add_option("--llm", *llm_model, "LLM model id (default: " + std::string(kDefaultRagLlm) + ")") + ->default_val(kDefaultRagLlm); + query_cmd->add_option("--embed", *embed_model, + "Embedding model id (default: " + std::string(kDefaultRagEmbed) + ")") + ->default_val(kDefaultRagEmbed); + query_cmd->add_option("--top-k", *top_k, "Number of chunks to retrieve"); + query_cmd->add_option("--max-tokens", *max_tokens, "Max answer tokens"); + query_cmd->add_option("--temperature", *temperature, "Sampling temperature"); + + query_cmd->callback([&options, question, docs, files, llm_model, embed_model, top_k, max_tokens, + temperature]() { + const int exit_code = run_rag_query(options, *llm_model, *embed_model, *docs, *files, + *question, *top_k, *max_tokens, *temperature); + if (exit_code != 0) { + throw CLI::RuntimeError(exit_code); + } + }); +} + +} // namespace rcli::commands diff --git a/sdk/runanywhere-cli/src/commands/commands.h b/sdk/runanywhere-cli/src/commands/commands.h index 017c0c908f..d8b47c446a 100644 --- a/sdk/runanywhere-cli/src/commands/commands.h +++ b/sdk/runanywhere-cli/src/commands/commands.h @@ -36,6 +36,7 @@ void register_vad(CLI::App& app, GlobalOptions& options); void register_voice(CLI::App& app, GlobalOptions& options); void register_serve(CLI::App& app, GlobalOptions& options); void register_lora(CLI::App& app, GlobalOptions& options); +void register_rag(CLI::App& app, GlobalOptions& options); /** * Shared pull flow (plan → start → progress → terminal state) for an diff --git a/sdk/runanywhere-commons/docs/architecture.html b/sdk/runanywhere-commons/docs/architecture.html new file mode 100644 index 0000000000..fb256ab8c8 --- /dev/null +++ b/sdk/runanywhere-commons/docs/architecture.html @@ -0,0 +1,224 @@ + + + + + +RunAnywhere SDK — Architecture Explorer + + + + +
+
+

RunAnywhere SDK — Architecture Explorer

+ pan & scroll to zoom · click any node +
+ + + + + +
+
+
+ +
+ + + + diff --git a/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_types.h b/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_types.h index 450431ae7a..ff7fb9dcdd 100644 --- a/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_types.h +++ b/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_types.h @@ -199,6 +199,9 @@ typedef struct rac_llm_result { /** Time to first token in milliseconds */ int64_t time_to_first_token_ms; + /** Prompt eval (prefill) time in milliseconds */ + int64_t prompt_eval_time_ms; + /** Total generation time in milliseconds */ int64_t total_time_ms; diff --git a/sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_types.h b/sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_types.h index 608e3ef66e..6f46a22275 100644 --- a/sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_types.h +++ b/sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_types.h @@ -345,6 +345,9 @@ typedef struct rac_vlm_result { /** Time to first token in milliseconds */ int64_t time_to_first_token_ms; + /** Prompt eval (multimodal prefill: image + prompt) time in milliseconds */ + int64_t prompt_eval_time_ms; + /** Time spent encoding the image in milliseconds */ int64_t image_encode_time_ms; diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h index 6c4f2243bc..281c17e1d6 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h @@ -122,10 +122,12 @@ typedef struct rac_telemetry_payload { // RAG-specific fields int32_t retrieved_docs_count; - // Embeddings-specific fields (embedding_model is read from model_id) + // Embeddings-specific fields (embedding_model is read from model_id; + // total_tokens reuses the shared LLM token field above) int32_t input_count; // texts embedded in the op int32_t vectors_produced; // vectors returned int32_t embedding_dimension; // vector dimension (via properties carrier) + int32_t batch_size; // texts submitted in the batch (via properties carrier) // RAG-specific extras (via properties carrier; retrieved_docs_count above) int32_t top_k; diff --git a/sdk/runanywhere-commons/src/features/embeddings/embeddings_module.cpp b/sdk/runanywhere-commons/src/features/embeddings/embeddings_module.cpp index a127d373d8..bc3d21b82a 100644 --- a/sdk/runanywhere-commons/src/features/embeddings/embeddings_module.cpp +++ b/sdk/runanywhere-commons/src/features/embeddings/embeddings_module.cpp @@ -99,7 +99,8 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons float progress, int64_t input_count, int64_t output_count, const char* error, double duration_ms = 0.0, int64_t embedding_dimension = 0, const char* model_id = nullptr, - const char* framework = nullptr) { + const char* framework = nullptr, int64_t total_tokens = 0, + int64_t batch_size = 0) { runanywhere::v1::SDKEvent event; event.set_id(event_id()); event.set_timestamp_ms(now_ms()); @@ -139,6 +140,12 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons if (embedding_dimension > 0) { (*event.mutable_properties())["embedding_dimension"] = std::to_string(embedding_dimension); } + if (total_tokens > 0) { + (*event.mutable_properties())["total_tokens"] = std::to_string(total_tokens); + } + if (batch_size > 0) { + (*event.mutable_properties())["batch_size"] = std::to_string(batch_size); + } publish_event(event); } @@ -251,7 +258,11 @@ rac_result_t rac_embeddings_embed_batch_proto(rac_handle_t handle, publish_capability(runanywhere::v1::CAPABILITY_OPERATION_EVENT_KIND_EMBEDDINGS_COMPLETED, "embeddings.embedBatch", 1.0f, static_cast(texts.size()), proto.vectors_size(), nullptr, - static_cast(result.processing_time_ms)); + static_cast(result.processing_time_ms), + static_cast(proto.dimension()), + proto.has_model_id() ? proto.model_id().c_str() : nullptr, nullptr, + static_cast(proto.tokens_used()), + static_cast(texts.size())); rac_embeddings_result_free(&result); return rc; #endif @@ -422,7 +433,8 @@ rac_result_t rac_embeddings_embed_batch_lifecycle_proto(const uint8_t* request_p 1.0f, static_cast(texts.size()), static_cast(result.vectors_size()), nullptr, static_cast(now_ms() - embed_start_ms), raw.num_embeddings > 0 ? static_cast(raw.embeddings[0].dimension) : 0, - ref.model_id, ref.framework_name); + ref.model_id, ref.framework_name, static_cast(result.tokens_used()), + static_cast(texts.size())); rc = copy_proto(result, out_result); rac_embeddings_result_free(&raw); rac::lifecycle::release_lifecycle_embeddings(&ref); diff --git a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp index cd95592559..36fa6f22bc 100644 --- a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp +++ b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp @@ -191,7 +191,8 @@ void emit_llm_generation_completed(const char* generation_id, const char* model_ double tokens_per_second, bool is_streaming, double time_to_first_token_ms, rac_inference_framework_t framework, float temperature, - int32_t max_tokens, int32_t context_length) { + int32_t max_tokens, int32_t context_length, + double prompt_eval_time_ms = 0.0) { runanywhere::v1::GenerationEvent g; g.set_kind(runanywhere::v1::GENERATION_EVENT_KIND_COMPLETED); if (model_id) @@ -205,6 +206,9 @@ void emit_llm_generation_completed(const char* generation_id, const char* model_ g.set_tokens_per_second(tokens_per_second); g.set_is_streaming(is_streaming); g.set_time_to_first_token_ms(static_cast(time_to_first_token_ms)); + if (prompt_eval_time_ms > 0.0) { + g.set_prompt_eval_time_ms(static_cast(prompt_eval_time_ms)); + } g.set_framework(rac::events::framework_to_proto_int(framework)); g.set_temperature(temperature); g.set_max_tokens(max_tokens); @@ -699,7 +703,8 @@ extern "C" rac_result_t rac_llm_component_generate(rac_handle_t handle, const ch generation_id.c_str(), model_id, model_name, out_result->prompt_tokens, out_result->completion_tokens, static_cast(total_time_ms), tokens_per_second, /*is_streaming=*/false, /*time_to_first_token_ms=*/0, component->actual_framework, - effective_options->temperature, effective_options->max_tokens, context_length); + effective_options->temperature, effective_options->max_tokens, context_length, + /*prompt_eval_time_ms=*/static_cast(out_result->prompt_eval_time_ms)); #endif return RAC_SUCCESS; @@ -999,6 +1004,7 @@ extern "C" rac_result_t rac_llm_component_generate_stream( auto ttft_duration = std::chrono::duration_cast( ctx.first_token_time - ctx.start_time); final_result.time_to_first_token_ms = ttft_duration.count(); + final_result.prompt_eval_time_ms = ttft_duration.count(); ttft_ms = static_cast(ttft_duration.count()); } @@ -1024,7 +1030,8 @@ extern "C" rac_result_t rac_llm_component_generate_stream( generation_id.c_str(), model_id, model_name, final_result.prompt_tokens, final_result.completion_tokens, static_cast(total_time_ms), tokens_per_second, /*is_streaming=*/true, ttft_ms, component->actual_framework, effective_options->temperature, - effective_options->max_tokens, context_length); + effective_options->max_tokens, context_length, + /*prompt_eval_time_ms=*/static_cast(final_result.prompt_eval_time_ms)); #endif // Terminal success event on the proto stream. @@ -1300,7 +1307,7 @@ void publish_generation_event(GenerationEventKind kind, const char* prompt, cons const char* framework_name = nullptr, double tokens_per_second = 0.0, double ttft_ms = 0.0, float temperature = -1.0f, int32_t max_tokens = 0, int32_t context_length = 0, - bool is_streaming = false) { + bool is_streaming = false, double prompt_eval_time_ms = 0.0) { SDKEvent event; const bool failed = kind == runanywhere::v1::GENERATION_EVENT_KIND_FAILED; populate_event_envelope(&event, runanywhere::v1::EVENT_CATEGORY_LLM, @@ -1350,6 +1357,9 @@ void publish_generation_event(GenerationEventKind kind, const char* prompt, cons if (ttft_ms > 0.0) { generation->set_time_to_first_token_ms(static_cast(ttft_ms)); } + if (prompt_eval_time_ms > 0.0) { + generation->set_prompt_eval_time_ms(static_cast(prompt_eval_time_ms)); + } // temperature 0.0 is a valid (greedy) setting, so the sentinel for "unset" // is a negative default — emit any non-negative value. if (temperature >= 0.0f) { @@ -2105,7 +2115,8 @@ rac_result_t rac_llm_generate_proto(const uint8_t* request_proto_bytes, size_t r raw.prompt_tokens > 0 ? raw.prompt_tokens : estimate_tokens(request.prompt().c_str()), ref.framework_name, static_cast(raw.tokens_per_second), static_cast(raw.time_to_first_token_ms), options.temperature, options.max_tokens, - lifecycle_context_length(ref), /*is_streaming=*/false); + lifecycle_context_length(ref), /*is_streaming=*/false, + /*prompt_eval_time_ms=*/static_cast(raw.prompt_eval_time_ms)); rac_llm_result_free(&raw); rac::llm::release_lifecycle_llm(&ref); @@ -2247,7 +2258,8 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, : 0.0, static_cast(stream_ttft), options.temperature, options.max_tokens, lifecycle_context_length(ref), - /*is_streaming=*/true); + /*is_streaming=*/true, + /*prompt_eval_time_ms=*/static_cast(stream_ttft)); } rac::llm::release_lifecycle_llm(&ref); diff --git a/sdk/runanywhere-commons/src/features/lora/rac_lora_service.cpp b/sdk/runanywhere-commons/src/features/lora/rac_lora_service.cpp index eb917ba654..3dd90dcee4 100644 --- a/sdk/runanywhere-commons/src/features/lora/rac_lora_service.cpp +++ b/sdk/runanywhere-commons/src/features/lora/rac_lora_service.cpp @@ -245,10 +245,40 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons publish_event(event); } -void publish_failure(rac_result_t code, const char* operation, const char* message) { +// Adapter file size for telemetry — read via ifstream (portable; no +// dependency). Best-effort: 0 if the path can't be opened. +int64_t adapter_file_size(const std::string& path) { + if (path.empty()) + return 0; + std::ifstream sz(path, std::ios::binary | std::ios::ate); + return sz ? static_cast(sz.tellg()) : 0; +} + +// adapter_id for telemetry attribution: prefer the catalog-linked id, else fall +// back to the adapter file's basename (without directory or .gguf extension) so +// the field is never blank when only a raw path was supplied. +std::string adapter_id_for(const runanywhere::v1::LoRAAdapterConfig& config) { + if (!config.adapter_id().empty()) + return config.adapter_id(); + const std::string& path = config.adapter_path(); + if (path.empty()) + return std::string(); + size_t slash = path.find_last_of("/\\"); + std::string base = (slash == std::string::npos) ? path : path.substr(slash + 1); + const std::string ext = ".gguf"; + if (base.size() > ext.size() && base.compare(base.size() - ext.size(), ext.size(), ext) == 0) { + base.resize(base.size() - ext.size()); + } + return base; +} + +void publish_failure(rac_result_t code, const char* operation, const char* message, + const char* model_id = nullptr, const char* adapter_id = nullptr, + int64_t adapter_size_bytes = 0) { publish_capability(runanywhere::v1::CAPABILITY_OPERATION_EVENT_KIND_LORA_FAILED, operation, (message != nullptr) && message[0] != '\0' ? message - : rac_error_message(code)); + : rac_error_message(code), + model_id, adapter_id, adapter_size_bytes); (void)rac_sdk_event_publish_failure(code, message, "llm", operation, RAC_TRUE); } @@ -569,7 +599,7 @@ rac_result_t rac_lora_apply_proto(const uint8_t* request_proto_bytes, size_t req mark_apply_error(&result, RAC_ERROR_INVALID_ARGUMENT, "LoRAApplyRequest.adapters is required"); publish_failure(RAC_ERROR_INVALID_ARGUMENT, "lora.apply", - "LoRAApplyRequest.adapters is required"); + "LoRAApplyRequest.adapters is required", base_model_id.c_str()); rac::llm::release_lifecycle_llm(&ref); return copy_proto(result, out_result); } @@ -580,7 +610,9 @@ rac_result_t rac_lora_apply_proto(const uint8_t* request_proto_bytes, size_t req auto* info = result.add_adapters(); *info = make_info(config, false, validation.message.c_str(), validation.code); mark_apply_error(&result, validation.code, validation.message.c_str()); - publish_failure(validation.code, "lora.apply", validation.message.c_str()); + publish_failure(validation.code, "lora.apply", validation.message.c_str(), + base_model_id.c_str(), adapter_id_for(config).c_str(), + adapter_file_size(config.adapter_path())); rac::llm::release_lifecycle_llm(&ref); return copy_proto(result, out_result); } @@ -591,14 +623,14 @@ rac_result_t rac_lora_apply_proto(const uint8_t* request_proto_bytes, size_t req mark_apply_error(&result, RAC_ERROR_NOT_SUPPORTED, "Backend does not support LoRA clear"); publish_failure(RAC_ERROR_NOT_SUPPORTED, "lora.apply", - "Backend does not support LoRA clear"); + "Backend does not support LoRA clear", base_model_id.c_str()); rac::llm::release_lifecycle_llm(&ref); return copy_proto(result, out_result); } rc = ref.ops->clear_lora(ref.impl); if (rc != RAC_SUCCESS) { mark_apply_error(&result, rc, rac_error_message(rc)); - publish_failure(rc, "lora.apply", rac_error_message(rc)); + publish_failure(rc, "lora.apply", rac_error_message(rc), base_model_id.c_str()); rac::llm::release_lifecycle_llm(&ref); return copy_proto(result, out_result); } @@ -614,7 +646,9 @@ rac_result_t rac_lora_apply_proto(const uint8_t* request_proto_bytes, size_t req auto* info = result.add_adapters(); *info = make_info(config, false, rac_error_message(rc), rc); mark_apply_error(&result, rc, rac_error_message(rc)); - publish_failure(rc, "lora.apply", rac_error_message(rc)); + publish_failure(rc, "lora.apply", rac_error_message(rc), base_model_id.c_str(), + adapter_id_for(config).c_str(), + adapter_file_size(config.adapter_path())); rac::llm::release_lifecycle_llm(&ref); return copy_proto(result, out_result); } @@ -623,18 +657,10 @@ rac_result_t rac_lora_apply_proto(const uint8_t* request_proto_bytes, size_t req track_lora_applied(backend_impl, base_model_id, applied_info); auto* info = result.add_adapters(); *info = applied_info; - // Adapter file size for telemetry — read via ifstream (portable; no - // dependency). Best-effort: 0 if the path can't be opened. - int64_t adapter_size = 0; - { - std::ifstream sz(config.adapter_path(), std::ios::binary | std::ios::ate); - if (sz) { - adapter_size = static_cast(sz.tellg()); - } - } publish_capability(runanywhere::v1::CAPABILITY_OPERATION_EVENT_KIND_LORA_ATTACHED, "lora.apply", nullptr, base_model_id.c_str(), - config.adapter_id().c_str(), adapter_size); + adapter_id_for(config).c_str(), + adapter_file_size(config.adapter_path())); } result.set_success(true); diff --git a/sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp b/sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp index 9b92bc907e..7bd175a590 100644 --- a/sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp +++ b/sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp @@ -907,7 +907,7 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons const char* framework = nullptr, double temperature = -1.0, int32_t max_tokens = 0, int64_t vision_tokens = 0, double vision_encode_ms = 0.0, const char* image_resolution = nullptr, - int32_t context_length = 0) { + int32_t context_length = 0, double prompt_eval_ms = 0.0) { runanywhere::v1::SDKEvent event; populate_envelope(&event, (error != nullptr && error[0] != '\0') ? runanywhere::v1::ERROR_SEVERITY_ERROR @@ -949,6 +949,9 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons if (ttft_ms > 0.0) { (*event.mutable_properties())["time_to_first_token_ms"] = std::to_string(ttft_ms); } + if (prompt_eval_ms > 0.0) { + (*event.mutable_properties())["prompt_eval_time_ms"] = std::to_string(prompt_eval_ms); + } // temperature=0.0 is a valid (greedy) setting, so a -1.0 sentinel marks // "not provided"; max_tokens 0 means unset. if (temperature >= 0.0) { @@ -1296,7 +1299,8 @@ rac_result_t rac_vlm_generate_proto(const uint8_t* request_proto_bytes, size_t r static_cast(result.time_to_first_token_ms()), ref.framework_name, static_cast(options.temperature), options.max_tokens, result.image_tokens(), static_cast(result.image_encode_time_ms()), - vlm_gen_res.empty() ? nullptr : vlm_gen_res.c_str(), raw.context_length); + vlm_gen_res.empty() ? nullptr : vlm_gen_res.c_str(), raw.context_length, + static_cast(raw.prompt_eval_time_ms)); rac_vlm_result_free(&raw); free_vlm_image(&image); rac_free(const_cast(prompt)); diff --git a/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.cc b/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.cc index 2013456099..cc35c37929 100644 --- a/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.cc +++ b/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.cc @@ -2219,11 +2219,11 @@ constexpr GenerationEvent::ParseTableT_ GenerationEvent::InternalGenerateParseTa { PROTOBUF_FIELD_OFFSET(GenerationEvent, _impl_._has_bits_), 0, // no _extensions_ - 33, 248, // max_field_number, fast_idx_mask + 34, 248, // max_field_number, fast_idx_mask offsetof(ParseTableT_, field_lookup_table), 0, // skipmap offsetof(ParseTableT_, field_entries), - 33, // num_field_entries + 34, // num_field_entries 0, // num_aux_entries offsetof(ParseTableT_, field_names), // no aux_entries class_data, @@ -2360,7 +2360,7 @@ constexpr GenerationEvent::ParseTableT_ GenerationEvent::InternalGenerateParseTa PROTOBUF_FIELD_OFFSET(GenerationEvent, _impl_.model_name_)}}, }}, {{ 33, 0, 1, - 65534, 32, + 65532, 32, 65535, 65535 }}, {{ // .runanywhere.v1.GenerationEventKind kind = 1; @@ -2428,7 +2428,9 @@ constexpr GenerationEvent::ParseTableT_ GenerationEvent::InternalGenerateParseTa // double duration_ms = 32; {PROTOBUF_FIELD_OFFSET(GenerationEvent, _impl_.duration_ms_), _Internal::kHasBitsOffset + 31, 0, (0 | ::_fl::kFcOptional | ::_fl::kDouble)}, // int32 framework = 33; - {PROTOBUF_FIELD_OFFSET(GenerationEvent, _impl_.framework_), _Internal::kHasBitsOffset + 32, 0, (0 | ::_fl::kFcOptional | ::_fl::kInt32)}, + {PROTOBUF_FIELD_OFFSET(GenerationEvent, _impl_.framework_), _Internal::kHasBitsOffset + 33, 0, (0 | ::_fl::kFcOptional | ::_fl::kInt32)}, + // int64 prompt_eval_time_ms = 34; + {PROTOBUF_FIELD_OFFSET(GenerationEvent, _impl_.prompt_eval_time_ms_), _Internal::kHasBitsOffset + 32, 0, (0 | ::_fl::kFcOptional | ::_fl::kInt64)}, }}, // no aux_entries {{ @@ -2526,6 +2528,7 @@ inline constexpr GenerationEvent::Impl_::Impl_( max_tokens_{0}, context_length_{0}, duration_ms_{0}, + prompt_eval_time_ms_{::int64_t{0}}, framework_{0} {} template @@ -7336,7 +7339,7 @@ const ::uint32_t 5, 0x081, // bitmap PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_._has_bits_), - 36, // hasbit index offset + 37, // hasbit index offset PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.kind_), PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.session_id_), PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.prompt_), @@ -7370,6 +7373,7 @@ const ::uint32_t PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.model_name_), PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.duration_ms_), PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.framework_), + PROTOBUF_FIELD_OFFSET(::runanywhere::v1::GenerationEvent, _impl_.prompt_eval_time_ms_), 17, 0, 1, @@ -7402,6 +7406,7 @@ const ::uint32_t 30, 16, 31, + 33, 32, 0x081, // bitmap PROTOBUF_FIELD_OFFSET(::runanywhere::v1::VoiceLifecycleEvent, _impl_._has_bits_), @@ -7921,29 +7926,29 @@ static const ::_pbi::MigrationSchema {108, sizeof(::runanywhere::v1::ComponentLifecycleEvent)}, {141, sizeof(::runanywhere::v1::SessionEvent)}, {158, sizeof(::runanywhere::v1::GenerationEvent)}, - {227, sizeof(::runanywhere::v1::VoiceLifecycleEvent)}, - {284, sizeof(::runanywhere::v1::CapabilityOperationEvent)}, - {307, sizeof(::runanywhere::v1::ModelEvent)}, - {342, sizeof(::runanywhere::v1::ModelRegistryEvent)}, - {375, sizeof(::runanywhere::v1::DownloadEvent)}, - {398, sizeof(::runanywhere::v1::StorageEvent)}, - {421, sizeof(::runanywhere::v1::StorageLifecycleEvent)}, - {444, sizeof(::runanywhere::v1::AuthEvent)}, - {457, sizeof(::runanywhere::v1::DeviceEvent)}, - {488, sizeof(::runanywhere::v1::NetworkEvent)}, - {503, sizeof(::runanywhere::v1::FrameworkEvent)}, - {522, sizeof(::runanywhere::v1::HardwareRoutingEvent)}, - {541, sizeof(::runanywhere::v1::PerformanceEvent)}, - {556, sizeof(::runanywhere::v1::TelemetryEvent_AttributesEntry_DoNotUse)}, - {563, sizeof(::runanywhere::v1::TelemetryEvent)}, - {576, sizeof(::runanywhere::v1::CancellationEvent)}, - {589, sizeof(::runanywhere::v1::FailureEvent)}, - {600, sizeof(::runanywhere::v1::SDKEvent_PropertiesEntry_DoNotUse)}, - {607, sizeof(::runanywhere::v1::SDKEvent)}, - {684, sizeof(::runanywhere::v1::SDKEventFilter)}, - {705, sizeof(::runanywhere::v1::SDKEventPublishRequest)}, - {712, sizeof(::runanywhere::v1::SDKEventPublishResult)}, - {725, sizeof(::runanywhere::v1::SDKEventSubscribeRequest)}, + {229, sizeof(::runanywhere::v1::VoiceLifecycleEvent)}, + {286, sizeof(::runanywhere::v1::CapabilityOperationEvent)}, + {309, sizeof(::runanywhere::v1::ModelEvent)}, + {344, sizeof(::runanywhere::v1::ModelRegistryEvent)}, + {377, sizeof(::runanywhere::v1::DownloadEvent)}, + {400, sizeof(::runanywhere::v1::StorageEvent)}, + {423, sizeof(::runanywhere::v1::StorageLifecycleEvent)}, + {446, sizeof(::runanywhere::v1::AuthEvent)}, + {459, sizeof(::runanywhere::v1::DeviceEvent)}, + {490, sizeof(::runanywhere::v1::NetworkEvent)}, + {505, sizeof(::runanywhere::v1::FrameworkEvent)}, + {524, sizeof(::runanywhere::v1::HardwareRoutingEvent)}, + {543, sizeof(::runanywhere::v1::PerformanceEvent)}, + {558, sizeof(::runanywhere::v1::TelemetryEvent_AttributesEntry_DoNotUse)}, + {565, sizeof(::runanywhere::v1::TelemetryEvent)}, + {578, sizeof(::runanywhere::v1::CancellationEvent)}, + {591, sizeof(::runanywhere::v1::FailureEvent)}, + {602, sizeof(::runanywhere::v1::SDKEvent_PropertiesEntry_DoNotUse)}, + {609, sizeof(::runanywhere::v1::SDKEvent)}, + {686, sizeof(::runanywhere::v1::SDKEventFilter)}, + {707, sizeof(::runanywhere::v1::SDKEventPublishRequest)}, + {714, sizeof(::runanywhere::v1::SDKEventPublishResult)}, + {727, sizeof(::runanywhere::v1::SDKEventSubscribeRequest)}, }; static const ::_pbi::MessageGlobalsBase* PROTOBUF_NONNULL const file_message_globals[] = { @@ -8054,7 +8059,7 @@ const char descriptor_table_protodef_sdk_5fevents_2eproto[] ABSL_ATTRIBUTE_SECTI "where.v1.SessionEventKind\022\022\n\nsession_id\030" "\002 \001(\t\022\017\n\007user_id\030\003 \001(\t\022\016\n\006reason\030\004 \001(\t\022\r" "\n\005error\030\005 \001(\t\022\025\n\rstarted_at_ms\030\006 \001(\003\022\023\n\013" - "ended_at_ms\030\007 \001(\003\"\227\006\n\017GenerationEvent\0221\n" + "ended_at_ms\030\007 \001(\003\"\264\006\n\017GenerationEvent\0221\n" "\004kind\030\001 \001(\0162#.runanywhere.v1.GenerationE" "ventKind\022\022\n\nsession_id\030\002 \001(\t\022\016\n\006prompt\030\003" " \001(\t\022\r\n\005token\030\004 \001(\t\022\026\n\016streaming_text\030\005 " @@ -8074,557 +8079,558 @@ const char descriptor_table_protodef_sdk_5fevents_2eproto[] ABSL_ATTRIBUTE_SECTI "\030\033 \001(\010\022\023\n\013temperature\030\034 \001(\002\022\022\n\nmax_token" "s\030\035 \001(\005\022\026\n\016context_length\030\036 \001(\005\022\022\n\nmodel" "_name\030\037 \001(\t\022\023\n\013duration_ms\030 \001(\001\022\021\n\tfram" - "ework\030! \001(\005\"\220\005\n\023VoiceLifecycleEvent\022,\n\004k" - "ind\030\001 \001(\0162\036.runanywhere.v1.VoiceEventKin" - "d\022\022\n\nsession_id\030\002 \001(\t\022\014\n\004text\030\003 \001(\t\022\022\n\nc" - "onfidence\030\004 \001(\002\022\025\n\rresponse_text\030\005 \001(\t\022\024" - "\n\014audio_base64\030\006 \001(\t\022\023\n\013duration_ms\030\007 \001(" - "\003\022\023\n\013audio_level\030\010 \001(\002\022\025\n\rtranscription\030" - "\t \001(\t\022\025\n\rturn_response\030\n \001(\t\022\031\n\021turn_aud" - "io_base64\030\013 \001(\t\022\r\n\005error\030\014 \001(\t\022\020\n\010model_" - "id\030\r \001(\t\022\022\n\nmodel_name\030\016 \001(\t\022\027\n\017audio_le" - "ngth_ms\030\017 \001(\003\022\030\n\020audio_size_bytes\030\020 \001(\005\022" - "\022\n\nword_count\030\021 \001(\005\022\030\n\020real_time_factor\030" - "\022 \001(\001\022\020\n\010language\030\023 \001(\t\022\023\n\013sample_rate\030\024" - " \001(\005\022\024\n\014is_streaming\030\025 \001(\010\022\021\n\tframework\030" - "\026 \001(\005\022\027\n\017character_count\030\027 \001(\005\022\031\n\021audio_" - "duration_ms\030\030 \001(\003\022\034\n\024audio_size_bytes_tt" - "s\030\031 \001(\005\022\036\n\026processing_duration_ms\030\032 \001(\003\022" - "\035\n\025characters_per_second\030\033 \001(\001\"\243\002\n\030Capab" - "ilityOperationEvent\022:\n\004kind\030\001 \001(\0162,.runa" - "nywhere.v1.CapabilityOperationEventKind\022" - "/\n\tcomponent\030\002 \001(\0162\034.runanywhere.v1.SDKC" - "omponent\022\020\n\010model_id\030\003 \001(\t\022\024\n\014operation_" - "id\030\004 \001(\t\022\021\n\toperation\030\005 \001(\t\022\020\n\010progress\030" - "\006 \001(\002\022\023\n\013input_count\030\007 \001(\003\022\024\n\014output_cou" - "nt\030\010 \001(\003\022\023\n\013result_json\030\t \001(\t\022\r\n\005error\030\n" - " \001(\t\"\371\002\n\nModelEvent\022,\n\004kind\030\001 \001(\0162\036.runa" - "nywhere.v1.ModelEventKind\022\020\n\010model_id\030\002 " - "\001(\t\022\017\n\007task_id\030\003 \001(\t\022\020\n\010progress\030\004 \001(\002\022\030" - "\n\020bytes_downloaded\030\005 \001(\003\022\023\n\013total_bytes\030" - "\006 \001(\003\022\026\n\016download_state\030\007 \001(\t\022\022\n\nlocal_p" - "ath\030\010 \001(\t\022\r\n\005error\030\t \001(\t\022\023\n\013model_count\030" - "\n \001(\005\022\031\n\021custom_model_name\030\013 \001(\t\022\030\n\020cust" - "om_model_url\030\014 \001(\t\022\022\n\nmodel_name\030\r \001(\t\022\030" - "\n\020model_size_bytes\030\016 \001(\003\022\023\n\013duration_ms\030" - "\017 \001(\003\022\021\n\tframework\030\020 \001(\005\"\322\005\n\022ModelRegist" - "ryEvent\0224\n\004kind\030\001 \001(\0162&.runanywhere.v1.M" - "odelRegistryEventKind\022\020\n\010model_id\030\002 \001(\t\022" - "\025\n\rassignment_id\030\003 \001(\t\0228\n\022assigned_compo" - "nent\030\004 \001(\0162\034.runanywhere.v1.SDKComponent" - "\0225\n\tframework\030\005 \001(\0162\".runanywhere.v1.Inf" - "erenceFramework\022\023\n\013source_path\030\006 \001(\t\022\r\n\005" - "error\030\007 \001(\t\022D\n\016refresh_result\030\024 \001(\0132*.ru" - "nanywhere.v1.ModelRegistryRefreshResultH" - "\000\0226\n\013list_result\030\025 \001(\0132\037.runanywhere.v1." - "ModelListResultH\000\0224\n\nget_result\030\026 \001(\0132\036." - "runanywhere.v1.ModelGetResultH\000\022:\n\rimpor" - "t_result\030\027 \001(\0132!.runanywhere.v1.ModelImp" - "ortResultH\000\022@\n\020discovery_result\030\030 \001(\0132$." - "runanywhere.v1.ModelDiscoveryResultH\000\022H\n" - "\024compatibility_result\030\031 \001(\0132(.runanywher" - "e.v1.ModelCompatibilityResultH\000\022B\n\024curre" - "nt_model_result\030\032 \001(\0132\".runanywhere.v1.C" - "urrentModelResultH\000B\010\n\006result\"\251\003\n\rDownlo" - "adEvent\022/\n\004kind\030\001 \001(\0162!.runanywhere.v1.D" - "ownloadEventKind\022\020\n\010model_id\030\002 \001(\t\022\017\n\007ta" - "sk_id\030\003 \001(\t\022\r\n\005error\030\004 \001(\t\0229\n\013plan_resul" - "t\030\024 \001(\0132\".runanywhere.v1.DownloadPlanRes" - "ultH\000\022;\n\014start_result\030\025 \001(\0132#.runanywher" - "e.v1.DownloadStartResultH\000\0224\n\010progress\030\026" - " \001(\0132 .runanywhere.v1.DownloadProgressH\000" - "\022=\n\rcancel_result\030\027 \001(\0132$.runanywhere.v1" - ".DownloadCancelResultH\000\022=\n\rresume_result" - "\030\030 \001(\0132$.runanywhere.v1.DownloadResumeRe" - "sultH\000B\t\n\007payload\"\374\001\n\014StorageEvent\022.\n\004ki" - "nd\030\001 \001(\0162 .runanywhere.v1.StorageEventKi" - "nd\022\020\n\010model_id\030\002 \001(\t\022\r\n\005error\030\003 \001(\t\022\023\n\013t" - "otal_bytes\030\004 \001(\003\022\027\n\017available_bytes\030\005 \001(" - "\003\022\022\n\nused_bytes\030\006 \001(\003\022\032\n\022stored_model_co" - "unt\030\007 \001(\005\022\021\n\tcache_key\030\010 \001(\t\022\025\n\revicted_" - "bytes\030\t \001(\003\022\023\n\013freed_bytes\030\n \001(\003\"\231\003\n\025Sto" - "rageLifecycleEvent\0227\n\004kind\030\001 \001(\0162).runan" - "ywhere.v1.StorageLifecycleEventKind\022\020\n\010m" - "odel_id\030\002 \001(\t\022\021\n\tcache_key\030\003 \001(\t\022\r\n\005byte" - "s\030\004 \001(\003\022\r\n\005error\030\005 \001(\t\0228\n\013info_result\030\024 " - "\001(\0132!.runanywhere.v1.StorageInfoResultH\000" - "\022H\n\023availability_result\030\025 \001(\0132).runanywh" - "ere.v1.StorageAvailabilityResultH\000\0228\n\013de" - "lete_plan\030\026 \001(\0132!.runanywhere.v1.Storage" - "DeletePlanH\000\022<\n\rdelete_result\030\027 \001(\0132#.ru" - "nanywhere.v1.StorageDeleteResultH\000B\010\n\006re" - "sult\"|\n\tAuthEvent\022+\n\004kind\030\001 \001(\0162\035.runany" - "where.v1.AuthEventKind\022\020\n\010provider\030\002 \001(\t" - "\022\022\n\nsubject_id\030\003 \001(\t\022\r\n\005scope\030\004 \001(\t\022\r\n\005e" - "rror\030\005 \001(\t\"\274\002\n\013DeviceEvent\022-\n\004kind\030\001 \001(\016" - "2\037.runanywhere.v1.DeviceEventKind\022\021\n\tdev" - "ice_id\030\002 \001(\t\022\017\n\007os_name\030\003 \001(\t\022\022\n\nos_vers" - "ion\030\004 \001(\t\022\r\n\005model\030\005 \001(\t\022\r\n\005error\030\006 \001(\t\022" - "\020\n\010property\030\007 \001(\t\022\021\n\tnew_value\030\010 \001(\t\022\021\n\t" - "old_value\030\t \001(\t\022\025\n\rbattery_level\030\n \001(\002\022\023" - "\n\013is_charging\030\013 \001(\010\022\025\n\rthermal_state\030\014 \001" - "(\t\022\024\n\014is_connected\030\r \001(\010\022\027\n\017connection_t" - "ype\030\016 \001(\t\"\226\001\n\014NetworkEvent\022.\n\004kind\030\001 \001(\016" - "2 .runanywhere.v1.NetworkEventKind\022\013\n\003ur" - "l\030\002 \001(\t\022\023\n\013status_code\030\003 \001(\005\022\021\n\tis_onlin" - "e\030\004 \001(\010\022\r\n\005error\030\005 \001(\t\022\022\n\nlatency_ms\030\006 \001" - "(\003\"\321\001\n\016FrameworkEvent\0220\n\004kind\030\001 \001(\0162\".ru" - "nanywhere.v1.FrameworkEventKind\022\021\n\tframe" - "work\030\002 \001(\005\022\024\n\014adapter_name\030\003 \001(\t\022\025\n\radap" - "ter_count\030\004 \001(\005\022\027\n\017framework_count\030\005 \001(\005" - "\022\023\n\013model_count\030\006 \001(\005\022\020\n\010modality\030\007 \001(\t\022" - "\r\n\005error\030\010 \001(\t\"\271\002\n\024HardwareRoutingEvent\022" - "6\n\004kind\030\001 \001(\0162(.runanywhere.v1.HardwareR" - "outingEventKind\022/\n\tcomponent\030\002 \001(\0162\034.run" - "anywhere.v1.SDKComponent\0225\n\tframework\030\003 " - "\001(\0162\".runanywhere.v1.InferenceFramework\022" - "\022\n\ncapability\030\004 \001(\t\022\r\n\005route\030\005 \001(\t\022\016\n\006re" - "ason\030\006 \001(\t\022\r\n\005error\030\007 \001(\t\022\?\n\020hardware_pr" - "ofile\030\024 \001(\0132%.runanywhere.v1.HardwarePro" - "fileResult\"\267\001\n\020PerformanceEvent\0222\n\004kind\030" - "\001 \001(\0162$.runanywhere.v1.PerformanceEventK" - "ind\022\024\n\014memory_bytes\030\002 \001(\003\022\025\n\rthermal_sta" - "te\030\003 \001(\t\022\021\n\toperation\030\004 \001(\t\022\024\n\014milliseco" - "nds\030\005 \001(\003\022\031\n\021tokens_per_second\030\006 \001(\001\"\344\001\n" - "\016TelemetryEvent\0220\n\004kind\030\001 \001(\0162\".runanywh" - "ere.v1.TelemetryEventKind\022\014\n\004name\030\002 \001(\t\022" - "B\n\nattributes\030\003 \003(\0132..runanywhere.v1.Tel" - "emetryEvent.AttributesEntry\022\r\n\005value\030\004 \001" - "(\001\022\014\n\004unit\030\005 \001(\t\0321\n\017AttributesEntry\022\013\n\003k" - "ey\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"\267\001\n\021Cancella" - "tionEvent\0223\n\004kind\030\001 \001(\0162%.runanywhere.v1" - ".CancellationEventKind\022/\n\tcomponent\030\002 \001(" - "\0162\034.runanywhere.v1.SDKComponent\022\024\n\014opera" - "tion_id\030\003 \001(\t\022\016\n\006reason\030\004 \001(\t\022\026\n\016user_in" - "itiated\030\005 \001(\010\"\220\001\n\014FailureEvent\022/\n\tcompon" - "ent\030\001 \001(\0162\034.runanywhere.v1.SDKComponent\022" - "\021\n\toperation\030\002 \001(\t\022\'\n\005error\030\003 \001(\0132\030.runa" - "nywhere.v1.SDKError\022\023\n\013recoverable\030\004 \001(\010" - "\"\233\016\n\010SDKEvent\022\024\n\014timestamp_ms\030\001 \001(\003\022/\n\010s" - "everity\030\002 \001(\0162\035.runanywhere.v1.ErrorSeve" - "rity\022/\n\010category\030\024 \001(\0162\035.runanywhere.v1." - "EventCategory\022/\n\tcomponent\030\025 \001(\0162\034.runan" - "ywhere.v1.SDKComponent\022,\n\005error\030\026 \001(\0132\030." - "runanywhere.v1.SDKErrorH\001\210\001\001\022\n\n\002id\030\r \001(\t" - "\022\022\n\nsession_id\030\016 \001(\t\0225\n\013destination\030\017 \001(" - "\0162 .runanywhere.v1.EventDestination\022<\n\np" - "roperties\030\020 \003(\0132(.runanywhere.v1.SDKEven" - "t.PropertiesEntry\022\024\n\014operation_id\030! \001(\t\022" - "\026\n\016correlation_id\030\" \001(\t\022\016\n\006source\030# \001(\t\022" - "\020\n\010trace_id\030$ \001(\t\022=\n\016initialization\030\003 \001(" - "\0132#.runanywhere.v1.InitializationEventH\000" - "\022;\n\rconfiguration\030\004 \001(\0132\".runanywhere.v1" - ".ConfigurationEventH\000\0225\n\ngeneration\030\005 \001(" - "\0132\037.runanywhere.v1.GenerationEventH\000\022+\n\005" - "model\030\006 \001(\0132\032.runanywhere.v1.ModelEventH" - "\000\0227\n\013performance\030\007 \001(\0132 .runanywhere.v1." - "PerformanceEventH\000\022/\n\007network\030\010 \001(\0132\034.ru" - "nanywhere.v1.NetworkEventH\000\022/\n\007storage\030\t" - " \001(\0132\034.runanywhere.v1.StorageEventH\000\0223\n\t" - "framework\030\n \001(\0132\036.runanywhere.v1.Framewo" - "rkEventH\000\022-\n\006device\030\013 \001(\0132\033.runanywhere." - "v1.DeviceEventH\000\022F\n\016component_init\030\014 \001(\013" - "2,.runanywhere.v1.ComponentInitializatio" - "nEventH\000\0224\n\005voice\030\021 \001(\0132#.runanywhere.v1" - ".VoiceLifecycleEventH\000\0224\n\016voice_pipeline" - "\030\022 \001(\0132\032.runanywhere.v1.VoiceEventH\000\022F\n\023" - "component_lifecycle\030\023 \001(\0132\'.runanywhere." - "v1.ComponentLifecycleEventH\000\022/\n\007session\030" - "\027 \001(\0132\034.runanywhere.v1.SessionEventH\000\022)\n" - "\004auth\030\030 \001(\0132\031.runanywhere.v1.AuthEventH\000" - "\022<\n\016model_registry\030\031 \001(\0132\".runanywhere.v" - "1.ModelRegistryEventH\000\0221\n\010download\030\032 \001(\013" - "2\035.runanywhere.v1.DownloadEventH\000\022B\n\021sto" - "rage_lifecycle\030\033 \001(\0132%.runanywhere.v1.St" - "orageLifecycleEventH\000\022@\n\020hardware_routin" - "g\030\034 \001(\0132$.runanywhere.v1.HardwareRouting" - "EventH\000\022>\n\ncapability\030\035 \001(\0132(.runanywher" - "e.v1.CapabilityOperationEventH\000\0223\n\ttelem" - "etry\030\036 \001(\0132\036.runanywhere.v1.TelemetryEve" - "ntH\000\0229\n\014cancellation\030\037 \001(\0132!.runanywhere" - ".v1.CancellationEventH\000\022/\n\007failure\030 \001(\013" - "2\034.runanywhere.v1.FailureEventH\000\0321\n\017Prop" - "ertiesEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:" - "\0028\001B\007\n\005eventB\010\n\006_error\"\312\002\n\016SDKEventFilte" - "r\0221\n\ncategories\030\001 \003(\0162\035.runanywhere.v1.E" - "ventCategory\0220\n\ncomponents\030\002 \003(\0162\034.runan" - "ywhere.v1.SDKComponent\0226\n\014destinations\030\003" - " \003(\0162 .runanywhere.v1.EventDestination\0227" - "\n\020minimum_severity\030\004 \001(\0162\035.runanywhere.v" - "1.ErrorSeverity\022\022\n\nsession_id\030\005 \001(\t\022\024\n\014o" - "peration_id\030\006 \001(\t\022\026\n\016correlation_id\030\007 \001(" - "\t\022\016\n\006source\030\010 \001(\t\022\020\n\010trace_id\030\t \001(\t\"]\n\026S" - "DKEventPublishRequest\022\'\n\005event\030\001 \001(\0132\030.r" - "unanywhere.v1.SDKEvent\022\032\n\022normalize_enve" - "lope\030\002 \001(\010\"\330\001\n\025SDKEventPublishResult\022\020\n\010" - "accepted\030\001 \001(\010\022\020\n\010event_id\030\002 \001(\t\0227\n\020norm" - "alized_event\030\003 \001(\0132\030.runanywhere.v1.SDKE" - "ventH\000\210\001\001\022\025\n\rerror_message\030\004 \001(\t\022,\n\005erro" - "r\030\005 \001(\0132\030.runanywhere.v1.SDKErrorH\001\210\001\001B\023" - "\n\021_normalized_eventB\010\n\006_error\"h\n\030SDKEven" - "tSubscribeRequest\022.\n\006filter\030\001 \001(\0132\036.runa" - "nywhere.v1.SDKEventFilter\022\034\n\024replay_queu" - "ed_events\030\002 \001(\010*\324\002\n\014SDKComponent\022\035\n\031SDK_" - "COMPONENT_UNSPECIFIED\020\000\022\025\n\021SDK_COMPONENT" - "_STT\020\001\022\025\n\021SDK_COMPONENT_TTS\020\002\022\025\n\021SDK_COM" - "PONENT_VAD\020\003\022\025\n\021SDK_COMPONENT_LLM\020\004\022\025\n\021S" - "DK_COMPONENT_VLM\020\005\022\033\n\027SDK_COMPONENT_DIFF" - "USION\020\006\022\025\n\021SDK_COMPONENT_RAG\020\007\022\034\n\030SDK_CO" - "MPONENT_EMBEDDINGS\020\010\022\035\n\031SDK_COMPONENT_VO" - "ICE_AGENT\020\t\022\032\n\026SDK_COMPONENT_WAKEWORD\020\n\022" - "%\n!SDK_COMPONENT_SPEAKER_DIARIZATION\020\013*\252" - "\001\n\020EventDestination\022!\n\035EVENT_DESTINATION" - "_UNSPECIFIED\020\000\022\034\n\030EVENT_DESTINATION_PUBL" - "IC\020\001\022\037\n\033EVENT_DESTINATION_TELEMETRY\020\002\022\031\n" - "\025EVENT_DESTINATION_ALL\020\003\022\031\n\025EVENT_DESTIN" - "ATION_LOG\020\004*\244\002\n\023InitializationStage\022$\n I" - "NITIALIZATION_STAGE_UNSPECIFIED\020\000\022 \n\034INI" - "TIALIZATION_STAGE_STARTED\020\001\022-\n)INITIALIZ" - "ATION_STAGE_CONFIGURATION_LOADED\020\002\022.\n*IN" - "ITIALIZATION_STAGE_SERVICES_BOOTSTRAPPED" - "\020\003\022\"\n\036INITIALIZATION_STAGE_COMPLETED\020\004\022\037" - "\n\033INITIALIZATION_STAGE_FAILED\020\005\022!\n\035INITI" - "ALIZATION_STAGE_SHUTDOWN\020\006*\223\007\n\026Configura" - "tionEventKind\022(\n$CONFIGURATION_EVENT_KIN" - "D_UNSPECIFIED\020\000\022*\n&CONFIGURATION_EVENT_K" - "IND_FETCH_STARTED\020\001\022,\n(CONFIGURATION_EVE" - "NT_KIND_FETCH_COMPLETED\020\002\022)\n%CONFIGURATI" - "ON_EVENT_KIND_FETCH_FAILED\020\003\022#\n\037CONFIGUR" - "ATION_EVENT_KIND_LOADED\020\004\022$\n CONFIGURATI" - "ON_EVENT_KIND_UPDATED\020\005\022)\n%CONFIGURATION" - "_EVENT_KIND_SYNC_STARTED\020\006\022+\n\'CONFIGURAT" - "ION_EVENT_KIND_SYNC_COMPLETED\020\007\022(\n$CONFI" - "GURATION_EVENT_KIND_SYNC_FAILED\020\010\022+\n\'CON" - "FIGURATION_EVENT_KIND_SYNC_REQUESTED\020\t\022/" - "\n+CONFIGURATION_EVENT_KIND_SETTINGS_REQU" - "ESTED\020\n\022/\n+CONFIGURATION_EVENT_KIND_SETT" - "INGS_RETRIEVED\020\013\0225\n1CONFIGURATION_EVENT_" - "KIND_ROUTING_POLICY_REQUESTED\020\014\0225\n1CONFI" - "GURATION_EVENT_KIND_ROUTING_POLICY_RETRI" - "EVED\020\r\0223\n/CONFIGURATION_EVENT_KIND_PRIVA" - "CY_MODE_REQUESTED\020\016\0223\n/CONFIGURATION_EVE" - "NT_KIND_PRIVACY_MODE_RETRIEVED\020\017\0227\n3CONF" - "IGURATION_EVENT_KIND_ANALYTICS_STATUS_RE" - "QUESTED\020\020\0227\n3CONFIGURATION_EVENT_KIND_AN" - "ALYTICS_STATUS_RETRIEVED\020\021\022$\n CONFIGURAT" - "ION_EVENT_KIND_CHANGED\020\022*\372\006\n ComponentIn" - "itializationEventKind\022)\n%COMPONENT_INIT_" - "EVENT_KIND_UNSPECIFIED\020\000\0224\n0COMPONENT_IN" - "IT_EVENT_KIND_INITIALIZATION_STARTED\020\001\0226" - "\n2COMPONENT_INIT_EVENT_KIND_INITIALIZATI" - "ON_COMPLETED\020\002\0225\n1COMPONENT_INIT_EVENT_K" - "IND_COMPONENT_STATE_CHANGED\020\003\0220\n,COMPONE" - "NT_INIT_EVENT_KIND_COMPONENT_CHECKING\020\004\022" - "9\n5COMPONENT_INIT_EVENT_KIND_COMPONENT_D" - "OWNLOAD_REQUIRED\020\005\0228\n4COMPONENT_INIT_EVE" - "NT_KIND_COMPONENT_DOWNLOAD_STARTED\020\006\0229\n5" - "COMPONENT_INIT_EVENT_KIND_COMPONENT_DOWN" - "LOAD_PROGRESS\020\007\022:\n6COMPONENT_INIT_EVENT_" - "KIND_COMPONENT_DOWNLOAD_COMPLETED\020\010\0224\n0C" - "OMPONENT_INIT_EVENT_KIND_COMPONENT_INITI" - "ALIZING\020\t\022-\n)COMPONENT_INIT_EVENT_KIND_C" - "OMPONENT_READY\020\n\022.\n*COMPONENT_INIT_EVENT" - "_KIND_COMPONENT_FAILED\020\013\0223\n/COMPONENT_IN" - "IT_EVENT_KIND_PARALLEL_INIT_STARTED\020\014\0225\n" - "1COMPONENT_INIT_EVENT_KIND_SEQUENTIAL_IN" - "IT_STARTED\020\r\0222\n.COMPONENT_INIT_EVENT_KIN" - "D_ALL_COMPONENTS_READY\020\016\0223\n/COMPONENT_IN" - "IT_EVENT_KIND_SOME_COMPONENTS_READY\020\017*\222\002" - "\n\020SessionEventKind\022\"\n\036SESSION_EVENT_KIND" - "_UNSPECIFIED\020\000\022\036\n\032SESSION_EVENT_KIND_CRE" - "ATED\020\001\022\036\n\032SESSION_EVENT_KIND_STARTED\020\002\022\036" - "\n\032SESSION_EVENT_KIND_RESUMED\020\003\022\035\n\031SESSIO" - "N_EVENT_KIND_PAUSED\020\004\022\034\n\030SESSION_EVENT_K" - "IND_ENDED\020\005\022\036\n\032SESSION_EVENT_KIND_EXPIRE" - "D\020\006\022\035\n\031SESSION_EVENT_KIND_FAILED\020\007*\324\010\n\023G" - "enerationEventKind\022%\n!GENERATION_EVENT_K" - "IND_UNSPECIFIED\020\000\022)\n%GENERATION_EVENT_KI" - "ND_SESSION_STARTED\020\001\022\'\n#GENERATION_EVENT" - "_KIND_SESSION_ENDED\020\002\022!\n\035GENERATION_EVEN" - "T_KIND_STARTED\020\003\022/\n+GENERATION_EVENT_KIN" - "D_FIRST_TOKEN_GENERATED\020\004\022)\n%GENERATION_" - "EVENT_KIND_TOKEN_GENERATED\020\005\022*\n&GENERATI" - "ON_EVENT_KIND_STREAMING_UPDATE\020\006\022#\n\037GENE" - "RATION_EVENT_KIND_COMPLETED\020\007\022 \n\034GENERAT" - "ION_EVENT_KIND_FAILED\020\010\022&\n\"GENERATION_EV" - "ENT_KIND_MODEL_LOADED\020\t\022(\n$GENERATION_EV" - "ENT_KIND_MODEL_UNLOADED\020\n\022)\n%GENERATION_" - "EVENT_KIND_COST_CALCULATED\020\013\022*\n&GENERATI" - "ON_EVENT_KIND_ROUTING_DECISION\020\014\022*\n&GENE" - "RATION_EVENT_KIND_STREAM_COMPLETED\020\r\022*\n&" - "GENERATION_EVENT_KIND_CANCEL_REQUESTED\020\016" - "\022#\n\037GENERATION_EVENT_KIND_CANCELLED\020\017\022+\n" - "\'GENERATION_EVENT_KIND_TOOL_CALL_STARTED" - "\020\020\022-\n)GENERATION_EVENT_KIND_TOOL_CALL_CO" - "MPLETED\020\021\022*\n&GENERATION_EVENT_KIND_TOOL_" - "CALL_FAILED\020\022\0223\n/GENERATION_EVENT_KIND_S" - "TRUCTURED_OUTPUT_STARTED\020\023\0225\n1GENERATION" - "_EVENT_KIND_STRUCTURED_OUTPUT_COMPLETED\020" - "\024\0222\n.GENERATION_EVENT_KIND_STRUCTURED_OU" - "TPUT_FAILED\020\025\022*\n&GENERATION_EVENT_KIND_T" - "HINKING_STARTED\020\026\022(\n$GENERATION_EVENT_KI" - "ND_THINKING_DELTA\020\027\022,\n(GENERATION_EVENT_" - "KIND_THINKING_COMPLETED\020\030*\332\017\n\016VoiceEvent" - "Kind\022 \n\034VOICE_EVENT_KIND_UNSPECIFIED\020\000\022&" - "\n\"VOICE_EVENT_KIND_LISTENING_STARTED\020\001\022$" - "\n VOICE_EVENT_KIND_LISTENING_ENDED\020\002\022$\n " - "VOICE_EVENT_KIND_SPEECH_DETECTED\020\003\022*\n&VO" - "ICE_EVENT_KIND_TRANSCRIPTION_STARTED\020\004\022*" - "\n&VOICE_EVENT_KIND_TRANSCRIPTION_PARTIAL" - "\020\005\022(\n$VOICE_EVENT_KIND_TRANSCRIPTION_FIN" - "AL\020\006\022\'\n#VOICE_EVENT_KIND_RESPONSE_GENERA" - "TED\020\007\022&\n\"VOICE_EVENT_KIND_SYNTHESIS_STAR" - "TED\020\010\022$\n VOICE_EVENT_KIND_AUDIO_GENERATE" - "D\020\t\022(\n$VOICE_EVENT_KIND_SYNTHESIS_COMPLE" - "TED\020\n\022%\n!VOICE_EVENT_KIND_SYNTHESIS_FAIL" - "ED\020\013\022%\n!VOICE_EVENT_KIND_PIPELINE_STARTE" - "D\020\014\022\'\n#VOICE_EVENT_KIND_PIPELINE_COMPLET" - "ED\020\r\022#\n\037VOICE_EVENT_KIND_PIPELINE_ERROR\020" - "\016\022 \n\034VOICE_EVENT_KIND_VAD_STARTED\020\017\022!\n\035V" - "OICE_EVENT_KIND_VAD_DETECTED\020\020\022\036\n\032VOICE_" - "EVENT_KIND_VAD_ENDED\020\021\022$\n VOICE_EVENT_KI" - "ND_VAD_INITIALIZED\020\022\022 \n\034VOICE_EVENT_KIND" - "_VAD_STOPPED\020\023\022#\n\037VOICE_EVENT_KIND_VAD_C" - "LEANED_UP\020\024\022#\n\037VOICE_EVENT_KIND_SPEECH_S" - "TARTED\020\025\022!\n\035VOICE_EVENT_KIND_SPEECH_ENDE" - "D\020\026\022#\n\037VOICE_EVENT_KIND_STT_PROCESSING\020\027" - "\022\'\n#VOICE_EVENT_KIND_STT_PARTIAL_RESULT\020" - "\030\022\"\n\036VOICE_EVENT_KIND_STT_COMPLETED\020\031\022\037\n" - "\033VOICE_EVENT_KIND_STT_FAILED\020\032\022#\n\037VOICE_" - "EVENT_KIND_LLM_PROCESSING\020\033\022#\n\037VOICE_EVE" - "NT_KIND_TTS_PROCESSING\020\034\022&\n\"VOICE_EVENT_" - "KIND_RECORDING_STARTED\020\035\022&\n\"VOICE_EVENT_" - "KIND_RECORDING_STOPPED\020\036\022%\n!VOICE_EVENT_" - "KIND_PLAYBACK_STARTED\020\037\022\'\n#VOICE_EVENT_K" - "IND_PLAYBACK_COMPLETED\020 \022%\n!VOICE_EVENT_" - "KIND_PLAYBACK_STOPPED\020!\022$\n VOICE_EVENT_K" - "IND_PLAYBACK_PAUSED\020\"\022%\n!VOICE_EVENT_KIN" - "D_PLAYBACK_RESUMED\020#\022$\n VOICE_EVENT_KIND" - "_PLAYBACK_FAILED\020$\022*\n&VOICE_EVENT_KIND_V" - "OICE_SESSION_STARTED\020%\022,\n(VOICE_EVENT_KI" - "ND_VOICE_SESSION_LISTENING\020&\0221\n-VOICE_EV" - "ENT_KIND_VOICE_SESSION_SPEECH_STARTED\020\'\022" - "/\n+VOICE_EVENT_KIND_VOICE_SESSION_SPEECH" - "_ENDED\020(\022-\n)VOICE_EVENT_KIND_VOICE_SESSI" - "ON_PROCESSING\020)\022.\n*VOICE_EVENT_KIND_VOIC" - "E_SESSION_TRANSCRIBED\020*\022,\n(VOICE_EVENT_K" - "IND_VOICE_SESSION_RESPONDED\020+\022+\n\'VOICE_E" - "VENT_KIND_VOICE_SESSION_SPEAKING\020,\0221\n-VO" - "ICE_EVENT_KIND_VOICE_SESSION_TURN_COMPLE" - "TED\020-\022*\n&VOICE_EVENT_KIND_VOICE_SESSION_" - "STOPPED\020.\022(\n$VOICE_EVENT_KIND_VOICE_SESS" - "ION_ERROR\020/\022\037\n\033VOICE_EVENT_KIND_VAD_PAUS" - "ED\0200\022 \n\034VOICE_EVENT_KIND_VAD_RESUMED\0201*\231" - "\010\n\034CapabilityOperationEventKind\022/\n+CAPAB" - "ILITY_OPERATION_EVENT_KIND_UNSPECIFIED\020\000" - "\022/\n+CAPABILITY_OPERATION_EVENT_KIND_VLM_" - "STARTED\020\001\0221\n-CAPABILITY_OPERATION_EVENT_" - "KIND_VLM_COMPLETED\020\002\022.\n*CAPABILITY_OPERA" - "TION_EVENT_KIND_VLM_FAILED\020\003\0225\n1CAPABILI" - "TY_OPERATION_EVENT_KIND_DIFFUSION_STARTE" - "D\020\004\0226\n2CAPABILITY_OPERATION_EVENT_KIND_D" - "IFFUSION_PROGRESS\020\005\0227\n3CAPABILITY_OPERAT" - "ION_EVENT_KIND_DIFFUSION_COMPLETED\020\006\0224\n0" - "CAPABILITY_OPERATION_EVENT_KIND_DIFFUSIO" - "N_FAILED\020\007\0226\n2CAPABILITY_OPERATION_EVENT" - "_KIND_EMBEDDINGS_STARTED\020\010\0228\n4CAPABILITY" - "_OPERATION_EVENT_KIND_EMBEDDINGS_COMPLET" - "ED\020\t\0225\n1CAPABILITY_OPERATION_EVENT_KIND_" - "EMBEDDINGS_FAILED\020\n\0229\n5CAPABILITY_OPERAT" - "ION_EVENT_KIND_RAG_INGESTION_STARTED\020\013\022;" - "\n7CAPABILITY_OPERATION_EVENT_KIND_RAG_IN" - "GESTION_COMPLETED\020\014\0225\n1CAPABILITY_OPERAT" - "ION_EVENT_KIND_RAG_QUERY_STARTED\020\r\0227\n3CA" - "PABILITY_OPERATION_EVENT_KIND_RAG_QUERY_" - "COMPLETED\020\016\022.\n*CAPABILITY_OPERATION_EVEN" - "T_KIND_RAG_FAILED\020\017\0221\n-CAPABILITY_OPERAT" - "ION_EVENT_KIND_LORA_ATTACHED\020\020\0221\n-CAPABI" - "LITY_OPERATION_EVENT_KIND_LORA_DETACHED\020" - "\021\022/\n+CAPABILITY_OPERATION_EVENT_KIND_LOR" - "A_FAILED\020\022*\361\007\n\016ModelEventKind\022 \n\034MODEL_E" - "VENT_KIND_UNSPECIFIED\020\000\022!\n\035MODEL_EVENT_K" - "IND_LOAD_STARTED\020\001\022\"\n\036MODEL_EVENT_KIND_L" - "OAD_PROGRESS\020\002\022#\n\037MODEL_EVENT_KIND_LOAD_" - "COMPLETED\020\003\022 \n\034MODEL_EVENT_KIND_LOAD_FAI" - "LED\020\004\022#\n\037MODEL_EVENT_KIND_UNLOAD_STARTED" - "\020\005\022%\n!MODEL_EVENT_KIND_UNLOAD_COMPLETED\020" - "\006\022\"\n\036MODEL_EVENT_KIND_UNLOAD_FAILED\020\007\022%\n" - "!MODEL_EVENT_KIND_DOWNLOAD_STARTED\020\010\022&\n\"" - "MODEL_EVENT_KIND_DOWNLOAD_PROGRESS\020\t\022\'\n#" - "MODEL_EVENT_KIND_DOWNLOAD_COMPLETED\020\n\022$\n" - " MODEL_EVENT_KIND_DOWNLOAD_FAILED\020\013\022\'\n#M" - "ODEL_EVENT_KIND_DOWNLOAD_CANCELLED\020\014\022#\n\037" - "MODEL_EVENT_KIND_LIST_REQUESTED\020\r\022#\n\037MOD" - "EL_EVENT_KIND_LIST_COMPLETED\020\016\022 \n\034MODEL_" - "EVENT_KIND_LIST_FAILED\020\017\022#\n\037MODEL_EVENT_" - "KIND_CATALOG_LOADED\020\020\022#\n\037MODEL_EVENT_KIN" - "D_DELETE_STARTED\020\021\022%\n!MODEL_EVENT_KIND_D" - "ELETE_COMPLETED\020\022\022\"\n\036MODEL_EVENT_KIND_DE" - "LETE_FAILED\020\023\022\'\n#MODEL_EVENT_KIND_CUSTOM" - "_MODEL_ADDED\020\024\022(\n$MODEL_EVENT_KIND_BUILT" - "_IN_REGISTERED\020\025\022\'\n#MODEL_EVENT_KIND_EXT" - "RACTION_STARTED\020\026\022(\n$MODEL_EVENT_KIND_EX" - "TRACTION_PROGRESS\020\027\022)\n%MODEL_EVENT_KIND_" - "EXTRACTION_COMPLETED\020\030\022&\n\"MODEL_EVENT_KI" - "ND_EXTRACTION_FAILED\020\031*\303\007\n\026ModelRegistry" - "EventKind\022)\n%MODEL_REGISTRY_EVENT_KIND_U" - "NSPECIFIED\020\000\022-\n)MODEL_REGISTRY_EVENT_KIN" - "D_REFRESH_STARTED\020\001\022/\n+MODEL_REGISTRY_EV" - "ENT_KIND_REFRESH_COMPLETED\020\002\022,\n(MODEL_RE" - "GISTRY_EVENT_KIND_REFRESH_FAILED\020\003\0220\n,MO" - "DEL_REGISTRY_EVENT_KIND_ASSIGNMENT_START" - "ED\020\004\0222\n.MODEL_REGISTRY_EVENT_KIND_ASSIGN" - "MENT_COMPLETED\020\005\022/\n+MODEL_REGISTRY_EVENT" - "_KIND_ASSIGNMENT_FAILED\020\006\022,\n(MODEL_REGIS" - "TRY_EVENT_KIND_IMPORT_STARTED\020\007\022.\n*MODEL" - "_REGISTRY_EVENT_KIND_IMPORT_COMPLETED\020\010\022" - "+\n\'MODEL_REGISTRY_EVENT_KIND_IMPORT_FAIL" - "ED\020\t\022/\n+MODEL_REGISTRY_EVENT_KIND_DISCOV" - "ERY_STARTED\020\n\0221\n-MODEL_REGISTRY_EVENT_KI" - "ND_DISCOVERY_COMPLETED\020\013\022.\n*MODEL_REGIST" - "RY_EVENT_KIND_DISCOVERY_FAILED\020\014\0223\n/MODE" - "L_REGISTRY_EVENT_KIND_CURRENT_MODEL_CHAN" - "GED\020\r\022*\n&MODEL_REGISTRY_EVENT_KIND_LIST_" - "STARTED\020\016\022,\n(MODEL_REGISTRY_EVENT_KIND_L" - "IST_COMPLETED\020\017\022)\n%MODEL_REGISTRY_EVENT_" - "KIND_LIST_FAILED\020\020\022)\n%MODEL_REGISTRY_EVE" - "NT_KIND_GET_STARTED\020\021\022+\n\'MODEL_REGISTRY_" - "EVENT_KIND_GET_COMPLETED\020\022\022(\n$MODEL_REGI" - "STRY_EVENT_KIND_GET_FAILED\020\023*\230\004\n\021Downloa" - "dEventKind\022#\n\037DOWNLOAD_EVENT_KIND_UNSPEC" - "IFIED\020\000\022$\n DOWNLOAD_EVENT_KIND_PLAN_STAR" - "TED\020\001\022&\n\"DOWNLOAD_EVENT_KIND_PLAN_COMPLE" - "TED\020\002\022#\n\037DOWNLOAD_EVENT_KIND_PLAN_FAILED" - "\020\003\022\037\n\033DOWNLOAD_EVENT_KIND_STARTED\020\004\022 \n\034D" - "OWNLOAD_EVENT_KIND_PROGRESS\020\005\022(\n$DOWNLOA" - "D_EVENT_KIND_CANCEL_REQUESTED\020\006\022!\n\035DOWNL" - "OAD_EVENT_KIND_CANCELLED\020\007\022(\n$DOWNLOAD_E" - "VENT_KIND_RESUME_REQUESTED\020\010\022\037\n\033DOWNLOAD" - "_EVENT_KIND_RESUMED\020\t\022!\n\035DOWNLOAD_EVENT_" - "KIND_COMPLETED\020\n\022\036\n\032DOWNLOAD_EVENT_KIND_" - "FAILED\020\013\022\036\n\032DOWNLOAD_EVENT_KIND_PAUSED\020\014" - "\022-\n)DOWNLOAD_EVENT_KIND_PARTIAL_BYTES_DE" - "LETED\020\r*\355\005\n\020StorageEventKind\022\"\n\036STORAGE_" - "EVENT_KIND_UNSPECIFIED\020\000\022%\n!STORAGE_EVEN" - "T_KIND_INFO_REQUESTED\020\001\022%\n!STORAGE_EVENT" - "_KIND_INFO_RETRIEVED\020\002\022\'\n#STORAGE_EVENT_" - "KIND_MODELS_REQUESTED\020\003\022\'\n#STORAGE_EVENT" - "_KIND_MODELS_RETRIEVED\020\004\022*\n&STORAGE_EVEN" - "T_KIND_CLEAR_CACHE_STARTED\020\005\022,\n(STORAGE_" - "EVENT_KIND_CLEAR_CACHE_COMPLETED\020\006\022)\n%ST" - "ORAGE_EVENT_KIND_CLEAR_CACHE_FAILED\020\007\022)\n" - "%STORAGE_EVENT_KIND_CLEAN_TEMP_STARTED\020\010" - "\022+\n\'STORAGE_EVENT_KIND_CLEAN_TEMP_COMPLE" - "TED\020\t\022(\n$STORAGE_EVENT_KIND_CLEAN_TEMP_F" - "AILED\020\n\022+\n\'STORAGE_EVENT_KIND_DELETE_MOD" - "EL_STARTED\020\013\022-\n)STORAGE_EVENT_KIND_DELET" - "E_MODEL_COMPLETED\020\014\022*\n&STORAGE_EVENT_KIN" - "D_DELETE_MODEL_FAILED\020\r\022 \n\034STORAGE_EVENT" - "_KIND_CACHE_HIT\020\016\022!\n\035STORAGE_EVENT_KIND_" - "CACHE_MISS\020\017\022\037\n\033STORAGE_EVENT_KIND_EVICT" - "ION\020\020\022 \n\034STORAGE_EVENT_KIND_DISK_FULL\020\021*" - "\371\005\n\031StorageLifecycleEventKind\022,\n(STORAGE" - "_LIFECYCLE_EVENT_KIND_UNSPECIFIED\020\000\022-\n)S" - "TORAGE_LIFECYCLE_EVENT_KIND_INFO_STARTED" - "\020\001\022/\n+STORAGE_LIFECYCLE_EVENT_KIND_INFO_" - "COMPLETED\020\002\0225\n1STORAGE_LIFECYCLE_EVENT_K" - "IND_AVAILABILITY_CHECKED\020\003\0224\n0STORAGE_LI" - "FECYCLE_EVENT_KIND_DELETE_PLAN_CREATED\020\004" - "\022/\n+STORAGE_LIFECYCLE_EVENT_KIND_DELETE_" - "STARTED\020\005\0221\n-STORAGE_LIFECYCLE_EVENT_KIN" - "D_DELETE_COMPLETED\020\006\022.\n*STORAGE_LIFECYCL" - "E_EVENT_KIND_DELETE_FAILED\020\007\0226\n2STORAGE_" - "LIFECYCLE_EVENT_KIND_CACHE_CLEANUP_START" - "ED\020\010\0228\n4STORAGE_LIFECYCLE_EVENT_KIND_CAC" - "HE_CLEANUP_COMPLETED\020\t\0225\n1STORAGE_LIFECY" - "CLE_EVENT_KIND_CACHE_CLEANUP_FAILED\020\n\0224\n" - "0STORAGE_LIFECYCLE_EVENT_KIND_AVAILABILI" - "TY_FAILED\020\013\0223\n/STORAGE_LIFECYCLE_EVENT_K" - "IND_DELETE_PLAN_FAILED\020\014\0229\n5STORAGE_LIFE" - "CYCLE_EVENT_KIND_DELETE_DRY_RUN_COMPLETE" - "D\020\r*\251\002\n\rAuthEventKind\022\037\n\033AUTH_EVENT_KIND" - "_UNSPECIFIED\020\000\022\035\n\031AUTH_EVENT_KIND_REQUES" - "TED\020\001\022\035\n\031AUTH_EVENT_KIND_SUCCEEDED\020\002\022\032\n\026" - "AUTH_EVENT_KIND_FAILED\020\003\022#\n\037AUTH_EVENT_K" - "IND_TOKEN_REFRESHED\020\004\022!\n\035AUTH_EVENT_KIND" - "_TOKEN_EXPIRED\020\005\022%\n!AUTH_EVENT_KIND_DEVI" - "CE_REGISTERED\020\006\022.\n*AUTH_EVENT_KIND_DEVIC" - "E_REGISTRATION_FAILED\020\007*\325\004\n\017DeviceEventK" - "ind\022!\n\035DEVICE_EVENT_KIND_UNSPECIFIED\020\000\022+" - "\n\'DEVICE_EVENT_KIND_DEVICE_INFO_COLLECTE" - "D\020\001\0223\n/DEVICE_EVENT_KIND_DEVICE_INFO_COL" - "LECTION_FAILED\020\002\022+\n\'DEVICE_EVENT_KIND_DE" - "VICE_INFO_REFRESHED\020\003\022.\n*DEVICE_EVENT_KI" - "ND_DEVICE_INFO_SYNC_STARTED\020\004\0220\n,DEVICE_" - "EVENT_KIND_DEVICE_INFO_SYNC_COMPLETED\020\005\022" - "-\n)DEVICE_EVENT_KIND_DEVICE_INFO_SYNC_FA" - "ILED\020\006\022*\n&DEVICE_EVENT_KIND_DEVICE_STATE" - "_CHANGED\020\007\022%\n!DEVICE_EVENT_KIND_BATTERY_" - "CHANGED\020\010\022%\n!DEVICE_EVENT_KIND_THERMAL_C" - "HANGED\020\t\022*\n&DEVICE_EVENT_KIND_CONNECTIVI" - "TY_CHANGED\020\n\022\'\n#DEVICE_EVENT_KIND_DEVICE" - "_REGISTERED\020\013\0220\n,DEVICE_EVENT_KIND_DEVIC" - "E_REGISTRATION_FAILED\020\014*\204\002\n\020NetworkEvent" - "Kind\022\"\n\036NETWORK_EVENT_KIND_UNSPECIFIED\020\000" - "\022&\n\"NETWORK_EVENT_KIND_REQUEST_STARTED\020\001" - "\022(\n$NETWORK_EVENT_KIND_REQUEST_COMPLETED" - "\020\002\022%\n!NETWORK_EVENT_KIND_REQUEST_FAILED\020" - "\003\022&\n\"NETWORK_EVENT_KIND_REQUEST_TIMEOUT\020" - "\004\022+\n\'NETWORK_EVENT_KIND_CONNECTIVITY_CHA" - "NGED\020\005*\272\005\n\022FrameworkEventKind\022$\n FRAMEWO" - "RK_EVENT_KIND_UNSPECIFIED\020\000\022+\n\'FRAMEWORK" - "_EVENT_KIND_ADAPTER_REGISTERED\020\001\022-\n)FRAM" - "EWORK_EVENT_KIND_ADAPTER_UNREGISTERED\020\002\022" - "+\n\'FRAMEWORK_EVENT_KIND_ADAPTERS_REQUEST" - "ED\020\003\022+\n\'FRAMEWORK_EVENT_KIND_ADAPTERS_RE" - "TRIEVED\020\004\022-\n)FRAMEWORK_EVENT_KIND_FRAMEW" - "ORKS_REQUESTED\020\005\022-\n)FRAMEWORK_EVENT_KIND" - "_FRAMEWORKS_RETRIEVED\020\006\022/\n+FRAMEWORK_EVE" - "NT_KIND_AVAILABILITY_REQUESTED\020\007\022/\n+FRAM" - "EWORK_EVENT_KIND_AVAILABILITY_RETRIEVED\020" - "\010\0227\n3FRAMEWORK_EVENT_KIND_MODELS_FOR_FRA" - "MEWORK_REQUESTED\020\t\0227\n3FRAMEWORK_EVENT_KI" - "ND_MODELS_FOR_FRAMEWORK_RETRIEVED\020\n\022:\n6F" - "RAMEWORK_EVENT_KIND_FRAMEWORKS_FOR_MODAL" - "ITY_REQUESTED\020\013\022:\n6FRAMEWORK_EVENT_KIND_" - "FRAMEWORKS_FOR_MODALITY_RETRIEVED\020\014\022\036\n\032F" - "RAMEWORK_EVENT_KIND_ERROR\020\r*\267\003\n\030Hardware" - "RoutingEventKind\022+\n\'HARDWARE_ROUTING_EVE" - "NT_KIND_UNSPECIFIED\020\000\022/\n+HARDWARE_ROUTIN" - "G_EVENT_KIND_PROFILE_STARTED\020\001\0221\n-HARDWA" - "RE_ROUTING_EVENT_KIND_PROFILE_COMPLETED\020" - "\002\022.\n*HARDWARE_ROUTING_EVENT_KIND_PROFILE" - "_FAILED\020\003\022.\n*HARDWARE_ROUTING_EVENT_KIND" - "_ROUTE_SELECTED\020\004\022-\n)HARDWARE_ROUTING_EV" - "ENT_KIND_ROUTE_CHANGED\020\005\022=\n9HARDWARE_ROU" - "TING_EVENT_KIND_FRAMEWORK_CAPABILITY_DET" - "ECTED\020\006\022<\n8HARDWARE_ROUTING_EVENT_KIND_F" - "RAMEWORK_CAPABILITY_MISSING\020\007*\370\001\n\024Perfor" - "manceEventKind\022&\n\"PERFORMANCE_EVENT_KIND" - "_UNSPECIFIED\020\000\022)\n%PERFORMANCE_EVENT_KIND" - "_MEMORY_WARNING\020\001\0220\n,PERFORMANCE_EVENT_K" - "IND_THERMAL_STATE_CHANGED\020\002\022+\n\'PERFORMAN" - "CE_EVENT_KIND_LATENCY_MEASURED\020\003\022.\n*PERF" - "ORMANCE_EVENT_KIND_THROUGHPUT_MEASURED\020\004" - "*\300\001\n\022TelemetryEventKind\022$\n TELEMETRY_EVE" - "NT_KIND_UNSPECIFIED\020\000\022 \n\034TELEMETRY_EVENT" - "_KIND_COUNTER\020\001\022\036\n\032TELEMETRY_EVENT_KIND_" - "GAUGE\020\002\022\"\n\036TELEMETRY_EVENT_KIND_HISTOGRA" - "M\020\003\022\036\n\032TELEMETRY_EVENT_KIND_TRACE\020\004*\334\001\n\025" - "CancellationEventKind\022\'\n#CANCELLATION_EV" - "ENT_KIND_UNSPECIFIED\020\000\022%\n!CANCELLATION_E" - "VENT_KIND_REQUESTED\020\001\022(\n$CANCELLATION_EV" - "ENT_KIND_ACKNOWLEDGED\020\002\022%\n!CANCELLATION_" - "EVENT_KIND_COMPLETED\020\003\022\"\n\036CANCELLATION_E" - "VENT_KIND_FAILED\020\0042\270\001\n\tSDKEvents\022X\n\007Publ" - "ish\022&.runanywhere.v1.SDKEventPublishRequ" - "est\032%.runanywhere.v1.SDKEventPublishResu" - "lt\022Q\n\tSubscribe\022(.runanywhere.v1.SDKEven" - "tSubscribeRequest\032\030.runanywhere.v1.SDKEv" - "ent0\001B\211\001\n\027ai.runanywhere.proto.v1B\016SdkEv" - "entsProtoP\001Z\n\n" + "capability\030\035 \001(\0132(.runanywhere.v1.Capabi" + "lityOperationEventH\000\0223\n\ttelemetry\030\036 \001(\0132" + "\036.runanywhere.v1.TelemetryEventH\000\0229\n\014can" + "cellation\030\037 \001(\0132!.runanywhere.v1.Cancell" + "ationEventH\000\022/\n\007failure\030 \001(\0132\034.runanywh" + "ere.v1.FailureEventH\000\0321\n\017PropertiesEntry" + "\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001B\007\n\005even" + "tB\010\n\006_error\"\312\002\n\016SDKEventFilter\0221\n\ncatego" + "ries\030\001 \003(\0162\035.runanywhere.v1.EventCategor" + "y\0220\n\ncomponents\030\002 \003(\0162\034.runanywhere.v1.S" + "DKComponent\0226\n\014destinations\030\003 \003(\0162 .runa" + "nywhere.v1.EventDestination\0227\n\020minimum_s" + "everity\030\004 \001(\0162\035.runanywhere.v1.ErrorSeve" + "rity\022\022\n\nsession_id\030\005 \001(\t\022\024\n\014operation_id" + "\030\006 \001(\t\022\026\n\016correlation_id\030\007 \001(\t\022\016\n\006source" + "\030\010 \001(\t\022\020\n\010trace_id\030\t \001(\t\"]\n\026SDKEventPubl" + "ishRequest\022\'\n\005event\030\001 \001(\0132\030.runanywhere." + "v1.SDKEvent\022\032\n\022normalize_envelope\030\002 \001(\010\"" + "\330\001\n\025SDKEventPublishResult\022\020\n\010accepted\030\001 " + "\001(\010\022\020\n\010event_id\030\002 \001(\t\0227\n\020normalized_even" + "t\030\003 \001(\0132\030.runanywhere.v1.SDKEventH\000\210\001\001\022\025" + "\n\rerror_message\030\004 \001(\t\022,\n\005error\030\005 \001(\0132\030.r" + "unanywhere.v1.SDKErrorH\001\210\001\001B\023\n\021_normaliz" + "ed_eventB\010\n\006_error\"h\n\030SDKEventSubscribeR" + "equest\022.\n\006filter\030\001 \001(\0132\036.runanywhere.v1." + "SDKEventFilter\022\034\n\024replay_queued_events\030\002" + " \001(\010*\324\002\n\014SDKComponent\022\035\n\031SDK_COMPONENT_U" + "NSPECIFIED\020\000\022\025\n\021SDK_COMPONENT_STT\020\001\022\025\n\021S" + "DK_COMPONENT_TTS\020\002\022\025\n\021SDK_COMPONENT_VAD\020" + "\003\022\025\n\021SDK_COMPONENT_LLM\020\004\022\025\n\021SDK_COMPONEN" + "T_VLM\020\005\022\033\n\027SDK_COMPONENT_DIFFUSION\020\006\022\025\n\021" + "SDK_COMPONENT_RAG\020\007\022\034\n\030SDK_COMPONENT_EMB" + "EDDINGS\020\010\022\035\n\031SDK_COMPONENT_VOICE_AGENT\020\t" + "\022\032\n\026SDK_COMPONENT_WAKEWORD\020\n\022%\n!SDK_COMP" + "ONENT_SPEAKER_DIARIZATION\020\013*\252\001\n\020EventDes" + "tination\022!\n\035EVENT_DESTINATION_UNSPECIFIE" + "D\020\000\022\034\n\030EVENT_DESTINATION_PUBLIC\020\001\022\037\n\033EVE" + "NT_DESTINATION_TELEMETRY\020\002\022\031\n\025EVENT_DEST" + "INATION_ALL\020\003\022\031\n\025EVENT_DESTINATION_LOG\020\004" + "*\244\002\n\023InitializationStage\022$\n INITIALIZATI" + "ON_STAGE_UNSPECIFIED\020\000\022 \n\034INITIALIZATION" + "_STAGE_STARTED\020\001\022-\n)INITIALIZATION_STAGE" + "_CONFIGURATION_LOADED\020\002\022.\n*INITIALIZATIO" + "N_STAGE_SERVICES_BOOTSTRAPPED\020\003\022\"\n\036INITI" + "ALIZATION_STAGE_COMPLETED\020\004\022\037\n\033INITIALIZ" + "ATION_STAGE_FAILED\020\005\022!\n\035INITIALIZATION_S" + "TAGE_SHUTDOWN\020\006*\223\007\n\026ConfigurationEventKi" + "nd\022(\n$CONFIGURATION_EVENT_KIND_UNSPECIFI" + "ED\020\000\022*\n&CONFIGURATION_EVENT_KIND_FETCH_S" + "TARTED\020\001\022,\n(CONFIGURATION_EVENT_KIND_FET" + "CH_COMPLETED\020\002\022)\n%CONFIGURATION_EVENT_KI" + "ND_FETCH_FAILED\020\003\022#\n\037CONFIGURATION_EVENT" + "_KIND_LOADED\020\004\022$\n CONFIGURATION_EVENT_KI" + "ND_UPDATED\020\005\022)\n%CONFIGURATION_EVENT_KIND" + "_SYNC_STARTED\020\006\022+\n\'CONFIGURATION_EVENT_K" + "IND_SYNC_COMPLETED\020\007\022(\n$CONFIGURATION_EV" + "ENT_KIND_SYNC_FAILED\020\010\022+\n\'CONFIGURATION_" + "EVENT_KIND_SYNC_REQUESTED\020\t\022/\n+CONFIGURA" + "TION_EVENT_KIND_SETTINGS_REQUESTED\020\n\022/\n+" + "CONFIGURATION_EVENT_KIND_SETTINGS_RETRIE" + "VED\020\013\0225\n1CONFIGURATION_EVENT_KIND_ROUTIN" + "G_POLICY_REQUESTED\020\014\0225\n1CONFIGURATION_EV" + "ENT_KIND_ROUTING_POLICY_RETRIEVED\020\r\0223\n/C" + "ONFIGURATION_EVENT_KIND_PRIVACY_MODE_REQ" + "UESTED\020\016\0223\n/CONFIGURATION_EVENT_KIND_PRI" + "VACY_MODE_RETRIEVED\020\017\0227\n3CONFIGURATION_E" + "VENT_KIND_ANALYTICS_STATUS_REQUESTED\020\020\0227" + "\n3CONFIGURATION_EVENT_KIND_ANALYTICS_STA" + "TUS_RETRIEVED\020\021\022$\n CONFIGURATION_EVENT_K" + "IND_CHANGED\020\022*\372\006\n ComponentInitializatio" + "nEventKind\022)\n%COMPONENT_INIT_EVENT_KIND_" + "UNSPECIFIED\020\000\0224\n0COMPONENT_INIT_EVENT_KI" + "ND_INITIALIZATION_STARTED\020\001\0226\n2COMPONENT" + "_INIT_EVENT_KIND_INITIALIZATION_COMPLETE" + "D\020\002\0225\n1COMPONENT_INIT_EVENT_KIND_COMPONE" + "NT_STATE_CHANGED\020\003\0220\n,COMPONENT_INIT_EVE" + "NT_KIND_COMPONENT_CHECKING\020\004\0229\n5COMPONEN" + "T_INIT_EVENT_KIND_COMPONENT_DOWNLOAD_REQ" + "UIRED\020\005\0228\n4COMPONENT_INIT_EVENT_KIND_COM" + "PONENT_DOWNLOAD_STARTED\020\006\0229\n5COMPONENT_I" + "NIT_EVENT_KIND_COMPONENT_DOWNLOAD_PROGRE" + "SS\020\007\022:\n6COMPONENT_INIT_EVENT_KIND_COMPON" + "ENT_DOWNLOAD_COMPLETED\020\010\0224\n0COMPONENT_IN" + "IT_EVENT_KIND_COMPONENT_INITIALIZING\020\t\022-" + "\n)COMPONENT_INIT_EVENT_KIND_COMPONENT_RE" + "ADY\020\n\022.\n*COMPONENT_INIT_EVENT_KIND_COMPO" + "NENT_FAILED\020\013\0223\n/COMPONENT_INIT_EVENT_KI" + "ND_PARALLEL_INIT_STARTED\020\014\0225\n1COMPONENT_" + "INIT_EVENT_KIND_SEQUENTIAL_INIT_STARTED\020" + "\r\0222\n.COMPONENT_INIT_EVENT_KIND_ALL_COMPO" + "NENTS_READY\020\016\0223\n/COMPONENT_INIT_EVENT_KI" + "ND_SOME_COMPONENTS_READY\020\017*\222\002\n\020SessionEv" + "entKind\022\"\n\036SESSION_EVENT_KIND_UNSPECIFIE" + "D\020\000\022\036\n\032SESSION_EVENT_KIND_CREATED\020\001\022\036\n\032S" + "ESSION_EVENT_KIND_STARTED\020\002\022\036\n\032SESSION_E" + "VENT_KIND_RESUMED\020\003\022\035\n\031SESSION_EVENT_KIN" + "D_PAUSED\020\004\022\034\n\030SESSION_EVENT_KIND_ENDED\020\005" + "\022\036\n\032SESSION_EVENT_KIND_EXPIRED\020\006\022\035\n\031SESS" + "ION_EVENT_KIND_FAILED\020\007*\324\010\n\023GenerationEv" + "entKind\022%\n!GENERATION_EVENT_KIND_UNSPECI" + "FIED\020\000\022)\n%GENERATION_EVENT_KIND_SESSION_" + "STARTED\020\001\022\'\n#GENERATION_EVENT_KIND_SESSI" + "ON_ENDED\020\002\022!\n\035GENERATION_EVENT_KIND_STAR" + "TED\020\003\022/\n+GENERATION_EVENT_KIND_FIRST_TOK" + "EN_GENERATED\020\004\022)\n%GENERATION_EVENT_KIND_" + "TOKEN_GENERATED\020\005\022*\n&GENERATION_EVENT_KI" + "ND_STREAMING_UPDATE\020\006\022#\n\037GENERATION_EVEN" + "T_KIND_COMPLETED\020\007\022 \n\034GENERATION_EVENT_K" + "IND_FAILED\020\010\022&\n\"GENERATION_EVENT_KIND_MO" + "DEL_LOADED\020\t\022(\n$GENERATION_EVENT_KIND_MO" + "DEL_UNLOADED\020\n\022)\n%GENERATION_EVENT_KIND_" + "COST_CALCULATED\020\013\022*\n&GENERATION_EVENT_KI" + "ND_ROUTING_DECISION\020\014\022*\n&GENERATION_EVEN" + "T_KIND_STREAM_COMPLETED\020\r\022*\n&GENERATION_" + "EVENT_KIND_CANCEL_REQUESTED\020\016\022#\n\037GENERAT" + "ION_EVENT_KIND_CANCELLED\020\017\022+\n\'GENERATION" + "_EVENT_KIND_TOOL_CALL_STARTED\020\020\022-\n)GENER" + "ATION_EVENT_KIND_TOOL_CALL_COMPLETED\020\021\022*" + "\n&GENERATION_EVENT_KIND_TOOL_CALL_FAILED" + "\020\022\0223\n/GENERATION_EVENT_KIND_STRUCTURED_O" + "UTPUT_STARTED\020\023\0225\n1GENERATION_EVENT_KIND" + "_STRUCTURED_OUTPUT_COMPLETED\020\024\0222\n.GENERA" + "TION_EVENT_KIND_STRUCTURED_OUTPUT_FAILED" + "\020\025\022*\n&GENERATION_EVENT_KIND_THINKING_STA" + "RTED\020\026\022(\n$GENERATION_EVENT_KIND_THINKING" + "_DELTA\020\027\022,\n(GENERATION_EVENT_KIND_THINKI" + "NG_COMPLETED\020\030*\332\017\n\016VoiceEventKind\022 \n\034VOI" + "CE_EVENT_KIND_UNSPECIFIED\020\000\022&\n\"VOICE_EVE" + "NT_KIND_LISTENING_STARTED\020\001\022$\n VOICE_EVE" + "NT_KIND_LISTENING_ENDED\020\002\022$\n VOICE_EVENT" + "_KIND_SPEECH_DETECTED\020\003\022*\n&VOICE_EVENT_K" + "IND_TRANSCRIPTION_STARTED\020\004\022*\n&VOICE_EVE" + "NT_KIND_TRANSCRIPTION_PARTIAL\020\005\022(\n$VOICE" + "_EVENT_KIND_TRANSCRIPTION_FINAL\020\006\022\'\n#VOI" + "CE_EVENT_KIND_RESPONSE_GENERATED\020\007\022&\n\"VO" + "ICE_EVENT_KIND_SYNTHESIS_STARTED\020\010\022$\n VO" + "ICE_EVENT_KIND_AUDIO_GENERATED\020\t\022(\n$VOIC" + "E_EVENT_KIND_SYNTHESIS_COMPLETED\020\n\022%\n!VO" + "ICE_EVENT_KIND_SYNTHESIS_FAILED\020\013\022%\n!VOI" + "CE_EVENT_KIND_PIPELINE_STARTED\020\014\022\'\n#VOIC" + "E_EVENT_KIND_PIPELINE_COMPLETED\020\r\022#\n\037VOI" + "CE_EVENT_KIND_PIPELINE_ERROR\020\016\022 \n\034VOICE_" + "EVENT_KIND_VAD_STARTED\020\017\022!\n\035VOICE_EVENT_" + "KIND_VAD_DETECTED\020\020\022\036\n\032VOICE_EVENT_KIND_" + "VAD_ENDED\020\021\022$\n VOICE_EVENT_KIND_VAD_INIT" + "IALIZED\020\022\022 \n\034VOICE_EVENT_KIND_VAD_STOPPE" + "D\020\023\022#\n\037VOICE_EVENT_KIND_VAD_CLEANED_UP\020\024" + "\022#\n\037VOICE_EVENT_KIND_SPEECH_STARTED\020\025\022!\n" + "\035VOICE_EVENT_KIND_SPEECH_ENDED\020\026\022#\n\037VOIC" + "E_EVENT_KIND_STT_PROCESSING\020\027\022\'\n#VOICE_E" + "VENT_KIND_STT_PARTIAL_RESULT\020\030\022\"\n\036VOICE_" + "EVENT_KIND_STT_COMPLETED\020\031\022\037\n\033VOICE_EVEN" + "T_KIND_STT_FAILED\020\032\022#\n\037VOICE_EVENT_KIND_" + "LLM_PROCESSING\020\033\022#\n\037VOICE_EVENT_KIND_TTS" + "_PROCESSING\020\034\022&\n\"VOICE_EVENT_KIND_RECORD" + "ING_STARTED\020\035\022&\n\"VOICE_EVENT_KIND_RECORD" + "ING_STOPPED\020\036\022%\n!VOICE_EVENT_KIND_PLAYBA" + "CK_STARTED\020\037\022\'\n#VOICE_EVENT_KIND_PLAYBAC" + "K_COMPLETED\020 \022%\n!VOICE_EVENT_KIND_PLAYBA" + "CK_STOPPED\020!\022$\n VOICE_EVENT_KIND_PLAYBAC" + "K_PAUSED\020\"\022%\n!VOICE_EVENT_KIND_PLAYBACK_" + "RESUMED\020#\022$\n VOICE_EVENT_KIND_PLAYBACK_F" + "AILED\020$\022*\n&VOICE_EVENT_KIND_VOICE_SESSIO" + "N_STARTED\020%\022,\n(VOICE_EVENT_KIND_VOICE_SE" + "SSION_LISTENING\020&\0221\n-VOICE_EVENT_KIND_VO" + "ICE_SESSION_SPEECH_STARTED\020\'\022/\n+VOICE_EV" + "ENT_KIND_VOICE_SESSION_SPEECH_ENDED\020(\022-\n" + ")VOICE_EVENT_KIND_VOICE_SESSION_PROCESSI" + "NG\020)\022.\n*VOICE_EVENT_KIND_VOICE_SESSION_T" + "RANSCRIBED\020*\022,\n(VOICE_EVENT_KIND_VOICE_S" + "ESSION_RESPONDED\020+\022+\n\'VOICE_EVENT_KIND_V" + "OICE_SESSION_SPEAKING\020,\0221\n-VOICE_EVENT_K" + "IND_VOICE_SESSION_TURN_COMPLETED\020-\022*\n&VO" + "ICE_EVENT_KIND_VOICE_SESSION_STOPPED\020.\022(" + "\n$VOICE_EVENT_KIND_VOICE_SESSION_ERROR\020/" + "\022\037\n\033VOICE_EVENT_KIND_VAD_PAUSED\0200\022 \n\034VOI" + "CE_EVENT_KIND_VAD_RESUMED\0201*\231\010\n\034Capabili" + "tyOperationEventKind\022/\n+CAPABILITY_OPERA" + "TION_EVENT_KIND_UNSPECIFIED\020\000\022/\n+CAPABIL" + "ITY_OPERATION_EVENT_KIND_VLM_STARTED\020\001\0221" + "\n-CAPABILITY_OPERATION_EVENT_KIND_VLM_CO" + "MPLETED\020\002\022.\n*CAPABILITY_OPERATION_EVENT_" + "KIND_VLM_FAILED\020\003\0225\n1CAPABILITY_OPERATIO" + "N_EVENT_KIND_DIFFUSION_STARTED\020\004\0226\n2CAPA" + "BILITY_OPERATION_EVENT_KIND_DIFFUSION_PR" + "OGRESS\020\005\0227\n3CAPABILITY_OPERATION_EVENT_K" + "IND_DIFFUSION_COMPLETED\020\006\0224\n0CAPABILITY_" + "OPERATION_EVENT_KIND_DIFFUSION_FAILED\020\007\022" + "6\n2CAPABILITY_OPERATION_EVENT_KIND_EMBED" + "DINGS_STARTED\020\010\0228\n4CAPABILITY_OPERATION_" + "EVENT_KIND_EMBEDDINGS_COMPLETED\020\t\0225\n1CAP" + "ABILITY_OPERATION_EVENT_KIND_EMBEDDINGS_" + "FAILED\020\n\0229\n5CAPABILITY_OPERATION_EVENT_K" + "IND_RAG_INGESTION_STARTED\020\013\022;\n7CAPABILIT" + "Y_OPERATION_EVENT_KIND_RAG_INGESTION_COM" + "PLETED\020\014\0225\n1CAPABILITY_OPERATION_EVENT_K" + "IND_RAG_QUERY_STARTED\020\r\0227\n3CAPABILITY_OP" + "ERATION_EVENT_KIND_RAG_QUERY_COMPLETED\020\016" + "\022.\n*CAPABILITY_OPERATION_EVENT_KIND_RAG_" + "FAILED\020\017\0221\n-CAPABILITY_OPERATION_EVENT_K" + "IND_LORA_ATTACHED\020\020\0221\n-CAPABILITY_OPERAT" + "ION_EVENT_KIND_LORA_DETACHED\020\021\022/\n+CAPABI" + "LITY_OPERATION_EVENT_KIND_LORA_FAILED\020\022*" + "\361\007\n\016ModelEventKind\022 \n\034MODEL_EVENT_KIND_U" + "NSPECIFIED\020\000\022!\n\035MODEL_EVENT_KIND_LOAD_ST" + "ARTED\020\001\022\"\n\036MODEL_EVENT_KIND_LOAD_PROGRES" + "S\020\002\022#\n\037MODEL_EVENT_KIND_LOAD_COMPLETED\020\003" + "\022 \n\034MODEL_EVENT_KIND_LOAD_FAILED\020\004\022#\n\037MO" + "DEL_EVENT_KIND_UNLOAD_STARTED\020\005\022%\n!MODEL" + "_EVENT_KIND_UNLOAD_COMPLETED\020\006\022\"\n\036MODEL_" + "EVENT_KIND_UNLOAD_FAILED\020\007\022%\n!MODEL_EVEN" + "T_KIND_DOWNLOAD_STARTED\020\010\022&\n\"MODEL_EVENT" + "_KIND_DOWNLOAD_PROGRESS\020\t\022\'\n#MODEL_EVENT" + "_KIND_DOWNLOAD_COMPLETED\020\n\022$\n MODEL_EVEN" + "T_KIND_DOWNLOAD_FAILED\020\013\022\'\n#MODEL_EVENT_" + "KIND_DOWNLOAD_CANCELLED\020\014\022#\n\037MODEL_EVENT" + "_KIND_LIST_REQUESTED\020\r\022#\n\037MODEL_EVENT_KI" + "ND_LIST_COMPLETED\020\016\022 \n\034MODEL_EVENT_KIND_" + "LIST_FAILED\020\017\022#\n\037MODEL_EVENT_KIND_CATALO" + "G_LOADED\020\020\022#\n\037MODEL_EVENT_KIND_DELETE_ST" + "ARTED\020\021\022%\n!MODEL_EVENT_KIND_DELETE_COMPL" + "ETED\020\022\022\"\n\036MODEL_EVENT_KIND_DELETE_FAILED" + "\020\023\022\'\n#MODEL_EVENT_KIND_CUSTOM_MODEL_ADDE" + "D\020\024\022(\n$MODEL_EVENT_KIND_BUILT_IN_REGISTE" + "RED\020\025\022\'\n#MODEL_EVENT_KIND_EXTRACTION_STA" + "RTED\020\026\022(\n$MODEL_EVENT_KIND_EXTRACTION_PR" + "OGRESS\020\027\022)\n%MODEL_EVENT_KIND_EXTRACTION_" + "COMPLETED\020\030\022&\n\"MODEL_EVENT_KIND_EXTRACTI" + "ON_FAILED\020\031*\303\007\n\026ModelRegistryEventKind\022)" + "\n%MODEL_REGISTRY_EVENT_KIND_UNSPECIFIED\020" + "\000\022-\n)MODEL_REGISTRY_EVENT_KIND_REFRESH_S" + "TARTED\020\001\022/\n+MODEL_REGISTRY_EVENT_KIND_RE" + "FRESH_COMPLETED\020\002\022,\n(MODEL_REGISTRY_EVEN" + "T_KIND_REFRESH_FAILED\020\003\0220\n,MODEL_REGISTR" + "Y_EVENT_KIND_ASSIGNMENT_STARTED\020\004\0222\n.MOD" + "EL_REGISTRY_EVENT_KIND_ASSIGNMENT_COMPLE" + "TED\020\005\022/\n+MODEL_REGISTRY_EVENT_KIND_ASSIG" + "NMENT_FAILED\020\006\022,\n(MODEL_REGISTRY_EVENT_K" + "IND_IMPORT_STARTED\020\007\022.\n*MODEL_REGISTRY_E" + "VENT_KIND_IMPORT_COMPLETED\020\010\022+\n\'MODEL_RE" + "GISTRY_EVENT_KIND_IMPORT_FAILED\020\t\022/\n+MOD" + "EL_REGISTRY_EVENT_KIND_DISCOVERY_STARTED" + "\020\n\0221\n-MODEL_REGISTRY_EVENT_KIND_DISCOVER" + "Y_COMPLETED\020\013\022.\n*MODEL_REGISTRY_EVENT_KI" + "ND_DISCOVERY_FAILED\020\014\0223\n/MODEL_REGISTRY_" + "EVENT_KIND_CURRENT_MODEL_CHANGED\020\r\022*\n&MO" + "DEL_REGISTRY_EVENT_KIND_LIST_STARTED\020\016\022," + "\n(MODEL_REGISTRY_EVENT_KIND_LIST_COMPLET" + "ED\020\017\022)\n%MODEL_REGISTRY_EVENT_KIND_LIST_F" + "AILED\020\020\022)\n%MODEL_REGISTRY_EVENT_KIND_GET" + "_STARTED\020\021\022+\n\'MODEL_REGISTRY_EVENT_KIND_" + "GET_COMPLETED\020\022\022(\n$MODEL_REGISTRY_EVENT_" + "KIND_GET_FAILED\020\023*\230\004\n\021DownloadEventKind\022" + "#\n\037DOWNLOAD_EVENT_KIND_UNSPECIFIED\020\000\022$\n " + "DOWNLOAD_EVENT_KIND_PLAN_STARTED\020\001\022&\n\"DO" + "WNLOAD_EVENT_KIND_PLAN_COMPLETED\020\002\022#\n\037DO" + "WNLOAD_EVENT_KIND_PLAN_FAILED\020\003\022\037\n\033DOWNL" + "OAD_EVENT_KIND_STARTED\020\004\022 \n\034DOWNLOAD_EVE" + "NT_KIND_PROGRESS\020\005\022(\n$DOWNLOAD_EVENT_KIN" + "D_CANCEL_REQUESTED\020\006\022!\n\035DOWNLOAD_EVENT_K" + "IND_CANCELLED\020\007\022(\n$DOWNLOAD_EVENT_KIND_R" + "ESUME_REQUESTED\020\010\022\037\n\033DOWNLOAD_EVENT_KIND" + "_RESUMED\020\t\022!\n\035DOWNLOAD_EVENT_KIND_COMPLE" + "TED\020\n\022\036\n\032DOWNLOAD_EVENT_KIND_FAILED\020\013\022\036\n" + "\032DOWNLOAD_EVENT_KIND_PAUSED\020\014\022-\n)DOWNLOA" + "D_EVENT_KIND_PARTIAL_BYTES_DELETED\020\r*\355\005\n" + "\020StorageEventKind\022\"\n\036STORAGE_EVENT_KIND_" + "UNSPECIFIED\020\000\022%\n!STORAGE_EVENT_KIND_INFO" + "_REQUESTED\020\001\022%\n!STORAGE_EVENT_KIND_INFO_" + "RETRIEVED\020\002\022\'\n#STORAGE_EVENT_KIND_MODELS" + "_REQUESTED\020\003\022\'\n#STORAGE_EVENT_KIND_MODEL" + "S_RETRIEVED\020\004\022*\n&STORAGE_EVENT_KIND_CLEA" + "R_CACHE_STARTED\020\005\022,\n(STORAGE_EVENT_KIND_" + "CLEAR_CACHE_COMPLETED\020\006\022)\n%STORAGE_EVENT" + "_KIND_CLEAR_CACHE_FAILED\020\007\022)\n%STORAGE_EV" + "ENT_KIND_CLEAN_TEMP_STARTED\020\010\022+\n\'STORAGE" + "_EVENT_KIND_CLEAN_TEMP_COMPLETED\020\t\022(\n$ST" + "ORAGE_EVENT_KIND_CLEAN_TEMP_FAILED\020\n\022+\n\'" + "STORAGE_EVENT_KIND_DELETE_MODEL_STARTED\020" + "\013\022-\n)STORAGE_EVENT_KIND_DELETE_MODEL_COM" + "PLETED\020\014\022*\n&STORAGE_EVENT_KIND_DELETE_MO" + "DEL_FAILED\020\r\022 \n\034STORAGE_EVENT_KIND_CACHE" + "_HIT\020\016\022!\n\035STORAGE_EVENT_KIND_CACHE_MISS\020" + "\017\022\037\n\033STORAGE_EVENT_KIND_EVICTION\020\020\022 \n\034ST" + "ORAGE_EVENT_KIND_DISK_FULL\020\021*\371\005\n\031Storage" + "LifecycleEventKind\022,\n(STORAGE_LIFECYCLE_" + "EVENT_KIND_UNSPECIFIED\020\000\022-\n)STORAGE_LIFE" + "CYCLE_EVENT_KIND_INFO_STARTED\020\001\022/\n+STORA" + "GE_LIFECYCLE_EVENT_KIND_INFO_COMPLETED\020\002" + "\0225\n1STORAGE_LIFECYCLE_EVENT_KIND_AVAILAB" + "ILITY_CHECKED\020\003\0224\n0STORAGE_LIFECYCLE_EVE" + "NT_KIND_DELETE_PLAN_CREATED\020\004\022/\n+STORAGE" + "_LIFECYCLE_EVENT_KIND_DELETE_STARTED\020\005\0221" + "\n-STORAGE_LIFECYCLE_EVENT_KIND_DELETE_CO" + "MPLETED\020\006\022.\n*STORAGE_LIFECYCLE_EVENT_KIN" + "D_DELETE_FAILED\020\007\0226\n2STORAGE_LIFECYCLE_E" + "VENT_KIND_CACHE_CLEANUP_STARTED\020\010\0228\n4STO" + "RAGE_LIFECYCLE_EVENT_KIND_CACHE_CLEANUP_" + "COMPLETED\020\t\0225\n1STORAGE_LIFECYCLE_EVENT_K" + "IND_CACHE_CLEANUP_FAILED\020\n\0224\n0STORAGE_LI" + "FECYCLE_EVENT_KIND_AVAILABILITY_FAILED\020\013" + "\0223\n/STORAGE_LIFECYCLE_EVENT_KIND_DELETE_" + "PLAN_FAILED\020\014\0229\n5STORAGE_LIFECYCLE_EVENT" + "_KIND_DELETE_DRY_RUN_COMPLETED\020\r*\251\002\n\rAut" + "hEventKind\022\037\n\033AUTH_EVENT_KIND_UNSPECIFIE" + "D\020\000\022\035\n\031AUTH_EVENT_KIND_REQUESTED\020\001\022\035\n\031AU" + "TH_EVENT_KIND_SUCCEEDED\020\002\022\032\n\026AUTH_EVENT_" + "KIND_FAILED\020\003\022#\n\037AUTH_EVENT_KIND_TOKEN_R" + "EFRESHED\020\004\022!\n\035AUTH_EVENT_KIND_TOKEN_EXPI" + "RED\020\005\022%\n!AUTH_EVENT_KIND_DEVICE_REGISTER" + "ED\020\006\022.\n*AUTH_EVENT_KIND_DEVICE_REGISTRAT" + "ION_FAILED\020\007*\325\004\n\017DeviceEventKind\022!\n\035DEVI" + "CE_EVENT_KIND_UNSPECIFIED\020\000\022+\n\'DEVICE_EV" + "ENT_KIND_DEVICE_INFO_COLLECTED\020\001\0223\n/DEVI" + "CE_EVENT_KIND_DEVICE_INFO_COLLECTION_FAI" + "LED\020\002\022+\n\'DEVICE_EVENT_KIND_DEVICE_INFO_R" + "EFRESHED\020\003\022.\n*DEVICE_EVENT_KIND_DEVICE_I" + "NFO_SYNC_STARTED\020\004\0220\n,DEVICE_EVENT_KIND_" + "DEVICE_INFO_SYNC_COMPLETED\020\005\022-\n)DEVICE_E" + "VENT_KIND_DEVICE_INFO_SYNC_FAILED\020\006\022*\n&D" + "EVICE_EVENT_KIND_DEVICE_STATE_CHANGED\020\007\022" + "%\n!DEVICE_EVENT_KIND_BATTERY_CHANGED\020\010\022%" + "\n!DEVICE_EVENT_KIND_THERMAL_CHANGED\020\t\022*\n" + "&DEVICE_EVENT_KIND_CONNECTIVITY_CHANGED\020" + "\n\022\'\n#DEVICE_EVENT_KIND_DEVICE_REGISTERED" + "\020\013\0220\n,DEVICE_EVENT_KIND_DEVICE_REGISTRAT" + "ION_FAILED\020\014*\204\002\n\020NetworkEventKind\022\"\n\036NET" + "WORK_EVENT_KIND_UNSPECIFIED\020\000\022&\n\"NETWORK" + "_EVENT_KIND_REQUEST_STARTED\020\001\022(\n$NETWORK" + "_EVENT_KIND_REQUEST_COMPLETED\020\002\022%\n!NETWO" + "RK_EVENT_KIND_REQUEST_FAILED\020\003\022&\n\"NETWOR" + "K_EVENT_KIND_REQUEST_TIMEOUT\020\004\022+\n\'NETWOR" + "K_EVENT_KIND_CONNECTIVITY_CHANGED\020\005*\272\005\n\022" + "FrameworkEventKind\022$\n FRAMEWORK_EVENT_KI" + "ND_UNSPECIFIED\020\000\022+\n\'FRAMEWORK_EVENT_KIND" + "_ADAPTER_REGISTERED\020\001\022-\n)FRAMEWORK_EVENT" + "_KIND_ADAPTER_UNREGISTERED\020\002\022+\n\'FRAMEWOR" + "K_EVENT_KIND_ADAPTERS_REQUESTED\020\003\022+\n\'FRA" + "MEWORK_EVENT_KIND_ADAPTERS_RETRIEVED\020\004\022-" + "\n)FRAMEWORK_EVENT_KIND_FRAMEWORKS_REQUES" + "TED\020\005\022-\n)FRAMEWORK_EVENT_KIND_FRAMEWORKS" + "_RETRIEVED\020\006\022/\n+FRAMEWORK_EVENT_KIND_AVA" + "ILABILITY_REQUESTED\020\007\022/\n+FRAMEWORK_EVENT" + "_KIND_AVAILABILITY_RETRIEVED\020\010\0227\n3FRAMEW" + "ORK_EVENT_KIND_MODELS_FOR_FRAMEWORK_REQU" + "ESTED\020\t\0227\n3FRAMEWORK_EVENT_KIND_MODELS_F" + "OR_FRAMEWORK_RETRIEVED\020\n\022:\n6FRAMEWORK_EV" + "ENT_KIND_FRAMEWORKS_FOR_MODALITY_REQUEST" + "ED\020\013\022:\n6FRAMEWORK_EVENT_KIND_FRAMEWORKS_" + "FOR_MODALITY_RETRIEVED\020\014\022\036\n\032FRAMEWORK_EV" + "ENT_KIND_ERROR\020\r*\267\003\n\030HardwareRoutingEven" + "tKind\022+\n\'HARDWARE_ROUTING_EVENT_KIND_UNS" + "PECIFIED\020\000\022/\n+HARDWARE_ROUTING_EVENT_KIN" + "D_PROFILE_STARTED\020\001\0221\n-HARDWARE_ROUTING_" + "EVENT_KIND_PROFILE_COMPLETED\020\002\022.\n*HARDWA" + "RE_ROUTING_EVENT_KIND_PROFILE_FAILED\020\003\022." + "\n*HARDWARE_ROUTING_EVENT_KIND_ROUTE_SELE" + "CTED\020\004\022-\n)HARDWARE_ROUTING_EVENT_KIND_RO" + "UTE_CHANGED\020\005\022=\n9HARDWARE_ROUTING_EVENT_" + "KIND_FRAMEWORK_CAPABILITY_DETECTED\020\006\022<\n8" + "HARDWARE_ROUTING_EVENT_KIND_FRAMEWORK_CA" + "PABILITY_MISSING\020\007*\370\001\n\024PerformanceEventK" + "ind\022&\n\"PERFORMANCE_EVENT_KIND_UNSPECIFIE" + "D\020\000\022)\n%PERFORMANCE_EVENT_KIND_MEMORY_WAR" + "NING\020\001\0220\n,PERFORMANCE_EVENT_KIND_THERMAL" + "_STATE_CHANGED\020\002\022+\n\'PERFORMANCE_EVENT_KI" + "ND_LATENCY_MEASURED\020\003\022.\n*PERFORMANCE_EVE" + "NT_KIND_THROUGHPUT_MEASURED\020\004*\300\001\n\022Teleme" + "tryEventKind\022$\n TELEMETRY_EVENT_KIND_UNS" + "PECIFIED\020\000\022 \n\034TELEMETRY_EVENT_KIND_COUNT" + "ER\020\001\022\036\n\032TELEMETRY_EVENT_KIND_GAUGE\020\002\022\"\n\036" + "TELEMETRY_EVENT_KIND_HISTOGRAM\020\003\022\036\n\032TELE" + "METRY_EVENT_KIND_TRACE\020\004*\334\001\n\025Cancellatio" + "nEventKind\022\'\n#CANCELLATION_EVENT_KIND_UN" + "SPECIFIED\020\000\022%\n!CANCELLATION_EVENT_KIND_R" + "EQUESTED\020\001\022(\n$CANCELLATION_EVENT_KIND_AC" + "KNOWLEDGED\020\002\022%\n!CANCELLATION_EVENT_KIND_" + "COMPLETED\020\003\022\"\n\036CANCELLATION_EVENT_KIND_F" + "AILED\020\0042\270\001\n\tSDKEvents\022X\n\007Publish\022&.runan" + "ywhere.v1.SDKEventPublishRequest\032%.runan" + "ywhere.v1.SDKEventPublishResult\022Q\n\tSubsc" + "ribe\022(.runanywhere.v1.SDKEventSubscribeR" + "equest\032\030.runanywhere.v1.SDKEvent0\001B\211\001\n\027a" + "i.runanywhere.proto.v1B\016SdkEventsProtoP\001" + "Z(&_impl_.duration_ms_) - reinterpret_cast(&_impl_.input_tokens_)) + sizeof(_impl_.duration_ms_)); } - _impl_.framework_ = 0; + cached_has_bits = _impl_._has_bits_[1]; + if (BatchCheckHasBit(cached_has_bits, 0x00000003U)) { + ::memset(&_impl_.prompt_eval_time_ms_, 0, static_cast<::size_t>( + reinterpret_cast(&_impl_.framework_) - + reinterpret_cast(&_impl_.prompt_eval_time_ms_)) + sizeof(_impl_.framework_)); + } _impl_._has_bits_.Clear(); _internal_metadata_.Clear<::google::protobuf::UnknownFieldSet>(); } @@ -13088,7 +13099,7 @@ ::uint8_t* PROTOBUF_NONNULL GenerationEvent::_InternalSerialize( cached_has_bits = this_._impl_._has_bits_[1]; // int32 framework = 33; - if (CheckHasBit(cached_has_bits, 0x00000001U)) { + if (CheckHasBit(cached_has_bits, 0x00000002U)) { if (this_._internal_framework() != 0) { target = stream->EnsureSpace(target); target = ::_pbi::WireFormatLite::WriteInt32ToArray( @@ -13096,6 +13107,15 @@ ::uint8_t* PROTOBUF_NONNULL GenerationEvent::_InternalSerialize( } } + // int64 prompt_eval_time_ms = 34; + if (CheckHasBit(cached_has_bits, 0x00000001U)) { + if (this_._internal_prompt_eval_time_ms() != 0) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteInt64ToArray( + 34, this_._internal_prompt_eval_time_ms(), target); + } + } + if (ABSL_PREDICT_FALSE(this_._internal_metadata_.have_unknown_fields())) { target = ::_pbi::WireFormat::InternalSerializeUnknownFieldsToArray( @@ -13347,10 +13367,17 @@ ::size_t GenerationEvent::ByteSizeLong() const { } } } - { - // int32 framework = 33; - cached_has_bits = this_._impl_._has_bits_[1]; + cached_has_bits = this_._impl_._has_bits_[1]; + if (BatchCheckHasBit(cached_has_bits, 0x00000003U)) { + // int64 prompt_eval_time_ms = 34; if (CheckHasBit(cached_has_bits, 0x00000001U)) { + if (this_._internal_prompt_eval_time_ms() != 0) { + total_size += 2 + ::_pbi::WireFormatLite::Int64Size( + this_._internal_prompt_eval_time_ms()); + } + } + // int32 framework = 33; + if (CheckHasBit(cached_has_bits, 0x00000002U)) { if (this_._internal_framework() != 0) { total_size += 2 + ::_pbi::WireFormatLite::Int32Size( this_._internal_framework()); @@ -13611,9 +13638,16 @@ void GenerationEvent::MergeImpl(::google::protobuf::MessageLite& to_msg, } } cached_has_bits = from._impl_._has_bits_[1]; - if (CheckHasBit(cached_has_bits, 0x00000001U)) { - if (from._internal_framework() != 0) { - _this->_impl_.framework_ = from._impl_.framework_; + if (BatchCheckHasBit(cached_has_bits, 0x00000003U)) { + if (CheckHasBit(cached_has_bits, 0x00000001U)) { + if (from._internal_prompt_eval_time_ms() != 0) { + _this->_impl_.prompt_eval_time_ms_ = from._impl_.prompt_eval_time_ms_; + } + } + if (CheckHasBit(cached_has_bits, 0x00000002U)) { + if (from._internal_framework() != 0) { + _this->_impl_.framework_ = from._impl_.framework_; + } } } _this->_impl_._has_bits_.Or(from._impl_._has_bits_); diff --git a/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.h b/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.h index 0b6cf057ca..988d7360e8 100644 --- a/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.h +++ b/sdk/runanywhere-commons/src/generated/proto/sdk_events.pb.h @@ -4639,6 +4639,7 @@ class PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED GenerationEvent final : public ::go kMaxTokensFieldNumber = 29, kContextLengthFieldNumber = 30, kDurationMsFieldNumber = 32, + kPromptEvalTimeMsFieldNumber = 34, kFrameworkFieldNumber = 33, }; // string session_id = 2; @@ -5045,6 +5046,16 @@ class PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED GenerationEvent final : public ::go double _internal_duration_ms() const; void _internal_set_duration_ms(double value); + public: + // int64 prompt_eval_time_ms = 34; + void clear_prompt_eval_time_ms() ; + [[nodiscard]] ::int64_t prompt_eval_time_ms() const; + void set_prompt_eval_time_ms(::int64_t value); + + private: + ::int64_t _internal_prompt_eval_time_ms() const; + void _internal_set_prompt_eval_time_ms(::int64_t value); + public: // int32 framework = 33; void clear_framework() ; @@ -5060,7 +5071,7 @@ class PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED GenerationEvent final : public ::go private: class _Internal; using ParseTableT_ = - ::google::protobuf::internal::TcParseTable<5, 33, + ::google::protobuf::internal::TcParseTable<5, 34, 0, 273, 7>; static constexpr ParseTableT_ InternalGenerateParseTable_( @@ -5121,6 +5132,7 @@ class PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED GenerationEvent final : public ::go ::int32_t max_tokens_; ::int32_t context_length_; double duration_ms_; + ::int64_t prompt_eval_time_ms_; ::int32_t framework_; PROTOBUF_TSAN_DECLARE_MEMBER }; @@ -17161,7 +17173,7 @@ inline void GenerationEvent::_internal_set_duration_ms(double value) { inline void GenerationEvent::clear_framework() { ::google::protobuf::internal::TSanWrite(&_impl_); _impl_.framework_ = 0; - ClearHasBit(_impl_._has_bits_[1], 0x00000001U); + ClearHasBit(_impl_._has_bits_[1], 0x00000002U); } inline ::int32_t GenerationEvent::framework() const { // @@protoc_insertion_point(field_get:runanywhere.v1.GenerationEvent.framework) @@ -17169,7 +17181,7 @@ inline ::int32_t GenerationEvent::framework() const { } inline void GenerationEvent::set_framework(::int32_t value) { _internal_set_framework(value); - SetHasBit(_impl_._has_bits_[1], 0x00000001U); + SetHasBit(_impl_._has_bits_[1], 0x00000002U); // @@protoc_insertion_point(field_set:runanywhere.v1.GenerationEvent.framework) } inline ::int32_t GenerationEvent::_internal_framework() const { @@ -17181,6 +17193,30 @@ inline void GenerationEvent::_internal_set_framework(::int32_t value) { _impl_.framework_ = value; } +// int64 prompt_eval_time_ms = 34; +inline void GenerationEvent::clear_prompt_eval_time_ms() { + ::google::protobuf::internal::TSanWrite(&_impl_); + _impl_.prompt_eval_time_ms_ = ::int64_t{0}; + ClearHasBit(_impl_._has_bits_[1], 0x00000001U); +} +inline ::int64_t GenerationEvent::prompt_eval_time_ms() const { + // @@protoc_insertion_point(field_get:runanywhere.v1.GenerationEvent.prompt_eval_time_ms) + return _internal_prompt_eval_time_ms(); +} +inline void GenerationEvent::set_prompt_eval_time_ms(::int64_t value) { + _internal_set_prompt_eval_time_ms(value); + SetHasBit(_impl_._has_bits_[1], 0x00000001U); + // @@protoc_insertion_point(field_set:runanywhere.v1.GenerationEvent.prompt_eval_time_ms) +} +inline ::int64_t GenerationEvent::_internal_prompt_eval_time_ms() const { + ::google::protobuf::internal::TSanRead(&_impl_); + return _impl_.prompt_eval_time_ms_; +} +inline void GenerationEvent::_internal_set_prompt_eval_time_ms(::int64_t value) { + ::google::protobuf::internal::TSanWrite(&_impl_); + _impl_.prompt_eval_time_ms_ = value; +} + // ------------------------------------------------------------------- // VoiceLifecycleEvent diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp index df5a8fc276..3249a27d69 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp @@ -7,6 +7,7 @@ * - Production (FastAPI): Uses id, timestamp, skips modality/device_id (batch level) */ +#include #include #include #include @@ -82,10 +83,13 @@ class JsonBuilder { } } - // Outputs double if is_valid is true, otherwise outputs null + // Outputs double if is_valid is true, otherwise outputs null. Non-finite + // values (NaN/Inf) are emitted as null — JSON has no nan/inf literals, and + // emitting them corrupts the whole batch (backend rejects with a JSON + // decode error), so a single unset metric must never poison the payload. void add_double_or_null(const char* key, double value, bool is_valid) { comma(); - if (is_valid) { + if (is_valid && std::isfinite(value)) { ss_ << "\"" << key << "\":" << value; } else { ss_ << "\"" << key << "\":null"; @@ -93,17 +97,22 @@ class JsonBuilder { } void add_double(const char* key, double value) { - if (value == 0.0) - return; // Skip zero values + if (value == 0.0 || !std::isfinite(value)) + return; // Skip zero and non-finite (NaN/Inf) — never emit invalid JSON. comma(); ss_ << "\"" << key << "\":" << value; } // Emit even when 0 — for fields where 0 is a meaningful measurement - // (e.g. temperature=0.0 greedy decode) rather than "unset". + // (e.g. temperature=0.0 greedy decode) rather than "unset". Non-finite + // still degrades to null so the JSON stays valid. void add_double_always(const char* key, double value) { comma(); - ss_ << "\"" << key << "\":" << value; + if (std::isfinite(value)) { + ss_ << "\"" << key << "\":" << value; + } else { + ss_ << "\"" << key << "\":null"; + } } void add_bool(const char* key, rac_bool_t value, rac_bool_t has_value) { @@ -335,12 +344,14 @@ rac_result_t rac_telemetry_manager_payload_to_json(const rac_telemetry_payload_t json.add_string("embedding_model", payload->embedding_model); json.add_bool("reranker_used", payload->reranker_used, payload->has_reranker_used); } else if (strcmp(modality, "embeddings") == 0) { - // input_count / vectors_produced / embedding_model / embedding_dimension - // have sources today (dimension via the properties carrier). - // total_tokens / batch_size still need a carrier. + // input_count / vectors_produced / embedding_model / embedding_dimension / + // total_tokens / batch_size all ride the properties carrier (dimension, + // total_tokens, batch_size) or the capability counts (input/vectors). json.add_int("input_count", payload->input_count); json.add_int("vectors_produced", payload->vectors_produced); json.add_int("embedding_dimension", payload->embedding_dimension); + json.add_int("total_tokens", payload->total_tokens); + json.add_int("batch_size", payload->batch_size); json.add_string("embedding_model", payload->model_id); } else if (strcmp(modality, "voice") == 0) { // Per-turn voice-agent pipeline summary (from MetricsEvent). @@ -359,8 +370,8 @@ rac_result_t rac_telemetry_manager_payload_to_json(const rac_telemetry_payload_t json.add_int("segment_count", payload->segment_count); json.add_int("sample_rate", payload->sample_rate); } else if (strcmp(modality, "lora") == 0) { - // base model rides on model_id; adapter_id + operation via the carrier. - // adapter_size_bytes still needs a source (would require stat-ing the file). + // base model rides on model_id; adapter_id + operation + adapter_size_bytes + // via the carrier (size is stat-ed from the adapter path in rac_lora_service). json.add_string("operation", payload->operation); json.add_string("base_model_id", payload->model_id); json.add_string("adapter_id", payload->adapter_id); diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp index 4ec2ec1c50..4829d64789 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp @@ -927,6 +927,7 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.time_to_first_token_ms = g.time_to_first_token_ms() != 0 ? static_cast(g.time_to_first_token_ms()) : static_cast(g.first_token_latency_ms()); + payload.prompt_eval_time_ms = static_cast(g.prompt_eval_time_ms()); payload.is_streaming = g.is_streaming() ? RAC_TRUE : RAC_FALSE; payload.has_is_streaming = RAC_TRUE; framework_str = framework_proto_to_string(g.framework()); @@ -1173,6 +1174,10 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, if (ttft_it != ev.properties().end()) { payload.time_to_first_token_ms = std::atof(ttft_it->second.c_str()); } + auto pe_it = ev.properties().find("prompt_eval_time_ms"); + if (pe_it != ev.properties().end()) { + payload.prompt_eval_time_ms = std::atof(pe_it->second.c_str()); + } auto temp_it = ev.properties().find("temperature"); if (temp_it != ev.properties().end()) { payload.temperature = std::atof(temp_it->second.c_str()); @@ -1230,12 +1235,23 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, // embedding_model is read from model_id (set above) in the JSON. payload.input_count = static_cast(c.input_count()); payload.vectors_produced = static_cast(c.output_count()); - // embedding_dimension rides the properties carrier (no proto field). + // embedding_dimension / total_tokens / batch_size ride the + // properties carrier (no CapabilityOperationEvent fields). auto dim_it = ev.properties().find("embedding_dimension"); if (dim_it != ev.properties().end()) { payload.embedding_dimension = static_cast(std::atoi(dim_it->second.c_str())); } + auto tok_it = ev.properties().find("total_tokens"); + if (tok_it != ev.properties().end()) { + payload.total_tokens = + static_cast(std::atoi(tok_it->second.c_str())); + } + auto bs_it = ev.properties().find("batch_size"); + if (bs_it != ev.properties().end()) { + payload.batch_size = + static_cast(std::atoi(bs_it->second.c_str())); + } break; } case runanywhere::v1::SDK_COMPONENT_DIFFUSION: { diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_events.pb.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_events.pb.dart index 505991687d..097deb6659 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_events.pb.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_events.pb.dart @@ -1353,6 +1353,7 @@ class GenerationEvent extends $pb.GeneratedMessage { $core.String? modelName, $core.double? durationMs, $core.int? framework, + $fixnum.Int64? promptEvalTimeMs, }) { final result = create(); if (kind != null) result.kind = kind; @@ -1392,6 +1393,7 @@ class GenerationEvent extends $pb.GeneratedMessage { if (modelName != null) result.modelName = modelName; if (durationMs != null) result.durationMs = durationMs; if (framework != null) result.framework = framework; + if (promptEvalTimeMs != null) result.promptEvalTimeMs = promptEvalTimeMs; return result; } @@ -1443,6 +1445,7 @@ class GenerationEvent extends $pb.GeneratedMessage { ..aOS(31, _omitFieldNames ? '' : 'modelName') ..aD(32, _omitFieldNames ? '' : 'durationMs') ..aI(33, _omitFieldNames ? '' : 'framework') + ..aInt64(34, _omitFieldNames ? '' : 'promptEvalTimeMs') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1778,6 +1781,15 @@ class GenerationEvent extends $pb.GeneratedMessage { $core.bool hasFramework() => $_has(32); @$pb.TagNumber(33) void clearFramework() => $_clearField(33); + + @$pb.TagNumber(34) + $fixnum.Int64 get promptEvalTimeMs => $_getI64(33); + @$pb.TagNumber(34) + set promptEvalTimeMs($fixnum.Int64 value) => $_setInt64(33, value); + @$pb.TagNumber(34) + $core.bool hasPromptEvalTimeMs() => $_has(33); + @$pb.TagNumber(34) + void clearPromptEvalTimeMs() => $_clearField(34); } /// --------------------------------------------------------------------------- diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/GenerationEvent.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/GenerationEvent.kt index 31231e931d..392eaa026e 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/GenerationEvent.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/GenerationEvent.kt @@ -357,6 +357,17 @@ public class GenerationEvent( schemaIndex = 32, ) public val framework: Int = 0, + /** + * prompt eval (prefill) duration + */ + @field:WireField( + tag = 34, + adapter = "com.squareup.wire.ProtoAdapter#INT64", + label = WireField.Label.OMIT_IDENTITY, + jsonName = "promptEvalTimeMs", + schemaIndex = 33, + ) + public val prompt_eval_time_ms: Long = 0L, unknownFields: ByteString = ByteString.EMPTY, ) : Message(ADAPTER, unknownFields) { @Deprecated( @@ -402,6 +413,7 @@ public class GenerationEvent( if (model_name != other.model_name) return false if (duration_ms != other.duration_ms) return false if (framework != other.framework) return false + if (prompt_eval_time_ms != other.prompt_eval_time_ms) return false return true } @@ -442,6 +454,7 @@ public class GenerationEvent( result = result * 37 + model_name.hashCode() result = result * 37 + duration_ms.hashCode() result = result * 37 + framework.hashCode() + result = result * 37 + prompt_eval_time_ms.hashCode() super.hashCode = result } return result @@ -482,6 +495,7 @@ public class GenerationEvent( result += """model_name=${sanitize(model_name)}""" result += """duration_ms=$duration_ms""" result += """framework=$framework""" + result += """prompt_eval_time_ms=$prompt_eval_time_ms""" return result.joinToString(prefix = "GenerationEvent{", separator = ", ", postfix = "}") } @@ -519,8 +533,9 @@ public class GenerationEvent( model_name: String = this.model_name, duration_ms: Double = this.duration_ms, framework: Int = this.framework, + prompt_eval_time_ms: Long = this.prompt_eval_time_ms, unknownFields: ByteString = this.unknownFields, - ): GenerationEvent = GenerationEvent(kind, session_id, prompt, token, streaming_text, tokens_count, response, tokens_used, latency_ms, first_token_latency_ms, error, model_id, cost_amount, cost_saved_amount, routing_target, routing_reason, cancel_reason, tool_call_id, tool_name, tool_payload_json, structured_schema_json, structured_output_json, thinking_text, input_tokens, tokens_per_second, time_to_first_token_ms, is_streaming, temperature, max_tokens, context_length, model_name, duration_ms, framework, unknownFields) + ): GenerationEvent = GenerationEvent(kind, session_id, prompt, token, streaming_text, tokens_count, response, tokens_used, latency_ms, first_token_latency_ms, error, model_id, cost_amount, cost_saved_amount, routing_target, routing_reason, cancel_reason, tool_call_id, tool_name, tool_payload_json, structured_schema_json, structured_output_json, thinking_text, input_tokens, tokens_per_second, time_to_first_token_ms, is_streaming, temperature, max_tokens, context_length, model_name, duration_ms, framework, prompt_eval_time_ms, unknownFields) public companion object { @JvmField @@ -633,6 +648,9 @@ public class GenerationEvent( if (value.framework != 0) { size += ProtoAdapter.INT32.encodedSizeWithTag(33, value.framework) } + if (value.prompt_eval_time_ms != 0L) { + size += ProtoAdapter.INT64.encodedSizeWithTag(34, value.prompt_eval_time_ms) + } return size } @@ -736,11 +754,17 @@ public class GenerationEvent( if (value.framework != 0) { ProtoAdapter.INT32.encodeWithTag(writer, 33, value.framework) } + if (value.prompt_eval_time_ms != 0L) { + ProtoAdapter.INT64.encodeWithTag(writer, 34, value.prompt_eval_time_ms) + } writer.writeBytes(value.unknownFields) } override fun encode(writer: ReverseProtoWriter, `value`: GenerationEvent) { writer.writeBytes(value.unknownFields) + if (value.prompt_eval_time_ms != 0L) { + ProtoAdapter.INT64.encodeWithTag(writer, 34, value.prompt_eval_time_ms) + } if (value.framework != 0) { ProtoAdapter.INT32.encodeWithTag(writer, 33, value.framework) } @@ -876,6 +900,7 @@ public class GenerationEvent( var model_name: String = "" var duration_ms: Double = 0.0 var framework: Int = 0 + var prompt_eval_time_ms: Long = 0L val unknownFields = reader.forEachTag { tag -> when (tag) { 1 -> try { @@ -915,6 +940,7 @@ public class GenerationEvent( 31 -> model_name = ProtoAdapter.STRING.decode(reader) 32 -> duration_ms = ProtoAdapter.DOUBLE.decode(reader) 33 -> framework = ProtoAdapter.INT32.decode(reader) + 34 -> prompt_eval_time_ms = ProtoAdapter.INT64.decode(reader) else -> reader.readUnknownField(tag) } } @@ -952,6 +978,7 @@ public class GenerationEvent( model_name = model_name, duration_ms = duration_ms, framework = framework, + prompt_eval_time_ms = prompt_eval_time_ms, unknownFields = unknownFields ) } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_events.pb.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_events.pb.swift index da89e4f992..355bd746a2 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_events.pb.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_events.pb.swift @@ -2503,6 +2503,12 @@ public nonisolated struct RAGenerationEvent: @unchecked Sendable { set {_uniqueStorage()._framework = newValue} } + /// prompt eval (prefill) duration + public var promptEvalTimeMs: Int64 { + get {_storage._promptEvalTimeMs} + set {_uniqueStorage()._promptEvalTimeMs = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -4645,7 +4651,7 @@ nonisolated extension RASessionEvent: SwiftProtobuf.Message, SwiftProtobuf._Mess nonisolated extension RAGenerationEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GenerationEvent" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}session_id\0\u{1}prompt\0\u{1}token\0\u{3}streaming_text\0\u{3}tokens_count\0\u{1}response\0\u{3}tokens_used\0\u{3}latency_ms\0\u{3}first_token_latency_ms\0\u{1}error\0\u{3}model_id\0\u{3}cost_amount\0\u{3}cost_saved_amount\0\u{3}routing_target\0\u{3}routing_reason\0\u{3}cancel_reason\0\u{3}tool_call_id\0\u{3}tool_name\0\u{3}tool_payload_json\0\u{3}structured_schema_json\0\u{3}structured_output_json\0\u{3}thinking_text\0\u{3}input_tokens\0\u{3}tokens_per_second\0\u{3}time_to_first_token_ms\0\u{3}is_streaming\0\u{1}temperature\0\u{3}max_tokens\0\u{3}context_length\0\u{3}model_name\0\u{3}duration_ms\0\u{1}framework\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}session_id\0\u{1}prompt\0\u{1}token\0\u{3}streaming_text\0\u{3}tokens_count\0\u{1}response\0\u{3}tokens_used\0\u{3}latency_ms\0\u{3}first_token_latency_ms\0\u{1}error\0\u{3}model_id\0\u{3}cost_amount\0\u{3}cost_saved_amount\0\u{3}routing_target\0\u{3}routing_reason\0\u{3}cancel_reason\0\u{3}tool_call_id\0\u{3}tool_name\0\u{3}tool_payload_json\0\u{3}structured_schema_json\0\u{3}structured_output_json\0\u{3}thinking_text\0\u{3}input_tokens\0\u{3}tokens_per_second\0\u{3}time_to_first_token_ms\0\u{3}is_streaming\0\u{1}temperature\0\u{3}max_tokens\0\u{3}context_length\0\u{3}model_name\0\u{3}duration_ms\0\u{1}framework\0\u{3}prompt_eval_time_ms\0") fileprivate class _StorageClass { var _kind: RAGenerationEventKind = .unspecified @@ -4681,6 +4687,7 @@ nonisolated extension RAGenerationEvent: SwiftProtobuf.Message, SwiftProtobuf._M var _modelName: String = String() var _durationMs: Double = 0 var _framework: Int32 = 0 + var _promptEvalTimeMs: Int64 = 0 // This property is used as the initial default value for new instances of the type. // The type itself is protecting the reference to its storage via CoW semantics. @@ -4724,6 +4731,7 @@ nonisolated extension RAGenerationEvent: SwiftProtobuf.Message, SwiftProtobuf._M _modelName = source._modelName _durationMs = source._durationMs _framework = source._framework + _promptEvalTimeMs = source._promptEvalTimeMs } } @@ -4775,6 +4783,7 @@ nonisolated extension RAGenerationEvent: SwiftProtobuf.Message, SwiftProtobuf._M case 31: try { try decoder.decodeSingularStringField(value: &_storage._modelName) }() case 32: try { try decoder.decodeSingularDoubleField(value: &_storage._durationMs) }() case 33: try { try decoder.decodeSingularInt32Field(value: &_storage._framework) }() + case 34: try { try decoder.decodeSingularInt64Field(value: &_storage._promptEvalTimeMs) }() default: break } } @@ -4882,6 +4891,9 @@ nonisolated extension RAGenerationEvent: SwiftProtobuf.Message, SwiftProtobuf._M if _storage._framework != 0 { try visitor.visitSingularInt32Field(value: _storage._framework, fieldNumber: 33) } + if _storage._promptEvalTimeMs != 0 { + try visitor.visitSingularInt64Field(value: _storage._promptEvalTimeMs, fieldNumber: 34) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4924,6 +4936,7 @@ nonisolated extension RAGenerationEvent: SwiftProtobuf.Message, SwiftProtobuf._M if _storage._modelName != rhs_storage._modelName {return false} if _storage._durationMs != rhs_storage._durationMs {return false} if _storage._framework != rhs_storage._framework {return false} + if _storage._promptEvalTimeMs != rhs_storage._promptEvalTimeMs {return false} return true } if !storagesAreEqual {return false} diff --git a/sdk/shared/proto-ts/src/sdk_events.ts b/sdk/shared/proto-ts/src/sdk_events.ts index 4493b0da36..39fac926f1 100644 --- a/sdk/shared/proto-ts/src/sdk_events.ts +++ b/sdk/shared/proto-ts/src/sdk_events.ts @@ -2687,6 +2687,8 @@ export interface GenerationEvent { durationMs: number; /** InferenceFramework enum int */ framework: number; + /** prompt eval (prefill) duration */ + promptEvalTimeMs: number; } /** @@ -4957,6 +4959,7 @@ function createBaseGenerationEvent(): GenerationEvent { modelName: "", durationMs: 0, framework: 0, + promptEvalTimeMs: 0, }; } @@ -5061,6 +5064,9 @@ export const GenerationEvent: MessageFns = { if (message.framework !== 0) { writer.uint32(264).int32(message.framework); } + if (message.promptEvalTimeMs !== 0) { + writer.uint32(272).int64(message.promptEvalTimeMs); + } return writer; }, @@ -5335,6 +5341,14 @@ export const GenerationEvent: MessageFns = { message.framework = reader.int32(); continue; } + case 34: { + if (tag !== 272) { + break; + } + + message.promptEvalTimeMs = longToNumber(reader.int64()); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -5483,6 +5497,11 @@ export const GenerationEvent: MessageFns = { ? globalThis.Number(object.duration_ms) : 0, framework: isSet(object.framework) ? globalThis.Number(object.framework) : 0, + promptEvalTimeMs: isSet(object.promptEvalTimeMs) + ? globalThis.Number(object.promptEvalTimeMs) + : isSet(object.prompt_eval_time_ms) + ? globalThis.Number(object.prompt_eval_time_ms) + : 0, }; }, @@ -5587,6 +5606,9 @@ export const GenerationEvent: MessageFns = { if (message.framework !== 0) { obj.framework = Math.round(message.framework); } + if (message.promptEvalTimeMs !== 0) { + obj.promptEvalTimeMs = Math.round(message.promptEvalTimeMs); + } return obj; }, @@ -5628,6 +5650,7 @@ export const GenerationEvent: MessageFns = { message.modelName = object.modelName ?? ""; message.durationMs = object.durationMs ?? 0; message.framework = object.framework ?? 0; + message.promptEvalTimeMs = object.promptEvalTimeMs ?? 0; return message; }, }; From 0e168f8db17f5ea6c36caf20ebef1c502972bacc Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 06:51:24 +0530 Subject: [PATCH 03/44] Fill RAG query_token_count and context_tokens telemetry fields at engine level --- .../telemetry/rac_telemetry_types.h | 2 ++ .../src/features/rag/rac_rag_proto_abi.cpp | 20 +++++++++++++++++-- .../telemetry/telemetry_json.cpp | 9 ++++++--- .../telemetry/telemetry_manager.cpp | 10 ++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h index 281c17e1d6..4ac041512b 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h @@ -132,6 +132,8 @@ typedef struct rac_telemetry_payload { // RAG-specific extras (via properties carrier; retrieved_docs_count above) int32_t top_k; double retrieval_time_ms; + int32_t query_token_count; // estimated tokens in the query (via properties carrier) + int32_t context_tokens; // estimated tokens in the retrieved context (via carrier) const char* embedding_model; // RAG embedding model (string → dup'd/freed) rac_bool_t reranker_used; // LLM-pointwise rerank enabled for the query rac_bool_t has_reranker_used; // whether reranker_used is set diff --git a/sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp b/sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp index 532fc9ffd4..daaa44b5cb 100644 --- a/sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp +++ b/sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp @@ -97,7 +97,8 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons const char* error, double duration_ms = 0.0, const char* model_id = nullptr, int64_t top_k = 0, double retrieval_time_ms = 0.0, const char* embedding_model = nullptr, - rac_result_t error_code = RAC_SUCCESS, int reranker_used = -1) { + rac_result_t error_code = RAC_SUCCESS, int reranker_used = -1, + int64_t query_token_count = 0, int64_t context_tokens = 0) { runanywhere::v1::SDKEvent event; event.set_id(event_id()); event.set_timestamp_ms(now_ms()); @@ -147,6 +148,12 @@ void publish_capability(runanywhere::v1::CapabilityOperationEventKind kind, cons if (reranker_used >= 0) { (*event.mutable_properties())["reranker_used"] = reranker_used != 0 ? "1" : "0"; } + if (query_token_count > 0) { + (*event.mutable_properties())["query_token_count"] = std::to_string(query_token_count); + } + if (context_tokens > 0) { + (*event.mutable_properties())["context_tokens"] = std::to_string(context_tokens); + } publish_event(event); } @@ -515,12 +522,21 @@ rac_result_t execute_rag_query(const std::shared_ptr& s, const int64_t effective_top_k = overrides.retrieval_top_k > 0 ? static_cast(overrides.retrieval_top_k) : static_cast(s->retrieval_top_k); + // Token counts use the same ~4-chars-per-token heuristic the RAG pipeline uses + // for its context budget (rag_pipeline_graph.cpp kCharsPerToken=4); there is no + // separate tokenizer at this ABI layer. query_token_count = the question; + // context_tokens = the assembled retrieved context passed to the LLM. + constexpr int64_t kCharsPerToken = 4; + const int64_t query_token_count = static_cast(question.size()) / kCharsPerToken; + const int64_t context_tokens = + static_cast(proto.context_used().size()) / kCharsPerToken; publish_capability(runanywhere::v1::CAPABILITY_OPERATION_EVENT_KIND_RAG_QUERY_COMPLETED, "rag.query", 1.0f, 1, proto.retrieved_chunks_size(), nullptr, total_ms, s->llm_model_id.empty() ? s->embedding_model_id.c_str() : s->llm_model_id.c_str(), effective_top_k, retrieval_ms, s->embedding_model_id.c_str(), - /*error_code=*/RAC_SUCCESS, /*reranker_used=*/s->rerank ? 1 : 0); + /*error_code=*/RAC_SUCCESS, /*reranker_used=*/s->rerank ? 1 : 0, + query_token_count, context_tokens); rac_llm_result_free(&llm_result); return RAC_SUCCESS; } diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp index 3249a27d69..79952e58f8 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp @@ -335,12 +335,15 @@ rac_result_t rac_telemetry_manager_payload_to_json(const rac_telemetry_payload_t json.add_double("vision_encode_time_ms", payload->vision_encode_time_ms); json.add_string("image_resolution", payload->image_resolution); } else if (strcmp(modality, "rag") == 0) { - // retrieved_docs_count / top_k / retrieval_time_ms / embedding_model have - // sources today (via the properties carrier). query_token_count / - // reranker_used / context_tokens still need carriers. + // retrieved_docs_count / top_k / retrieval_time_ms / embedding_model / + // reranker_used / query_token_count / context_tokens all ride the + // properties carrier (token counts estimated ~4 chars/token in + // rac_rag_proto_abi, matching the pipeline's context budget). json.add_int("retrieved_docs_count", payload->retrieved_docs_count); json.add_int("top_k", payload->top_k); json.add_double("retrieval_time_ms", payload->retrieval_time_ms); + json.add_int("query_token_count", payload->query_token_count); + json.add_int("context_tokens", payload->context_tokens); json.add_string("embedding_model", payload->embedding_model); json.add_bool("reranker_used", payload->reranker_used, payload->has_reranker_used); } else if (strcmp(modality, "embeddings") == 0) { diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp index 4829d64789..25bb4a4fe7 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp @@ -1228,6 +1228,16 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.reranker_used = rr_it->second == "1" ? RAC_TRUE : RAC_FALSE; payload.has_reranker_used = RAC_TRUE; } + auto qt_it = ev.properties().find("query_token_count"); + if (qt_it != ev.properties().end()) { + payload.query_token_count = + static_cast(std::atoi(qt_it->second.c_str())); + } + auto ct_it = ev.properties().find("context_tokens"); + if (ct_it != ev.properties().end()) { + payload.context_tokens = + static_cast(std::atoi(ct_it->second.c_str())); + } break; } case runanywhere::v1::SDK_COMPONENT_EMBEDDINGS: { From 4ff510a5324da58c6253ddf2e857262da9d1493a Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 08:04:48 +0530 Subject: [PATCH 04/44] Add rcli bench command auto-benchmarking installed LLM/STT/TTS/VLM models; fix LLM result UTF-8 sanitization and unary prompt_eval --- sdk/runanywhere-cli/CMakeLists.txt | 1 + sdk/runanywhere-cli/src/app.cpp | 1 + .../src/commands/cmd_bench.cpp | 741 ++++++++++++++++++ sdk/runanywhere-cli/src/commands/commands.h | 1 + .../src/features/llm/llm_module.cpp | 59 +- 5 files changed, 798 insertions(+), 5 deletions(-) create mode 100644 sdk/runanywhere-cli/src/commands/cmd_bench.cpp diff --git a/sdk/runanywhere-cli/CMakeLists.txt b/sdk/runanywhere-cli/CMakeLists.txt index db230290c0..c02e79b433 100644 --- a/sdk/runanywhere-cli/CMakeLists.txt +++ b/sdk/runanywhere-cli/CMakeLists.txt @@ -39,6 +39,7 @@ set(RCLI_SOURCES src/commands/cmd_vad.cpp src/commands/cmd_voice.cpp src/commands/cmd_rag.cpp + src/commands/cmd_bench.cpp src/commands/engine_options.cpp src/commands/model_setup.cpp src/config/cli_paths.cpp diff --git a/sdk/runanywhere-cli/src/app.cpp b/sdk/runanywhere-cli/src/app.cpp index 4852ba89df..35e2092c54 100644 --- a/sdk/runanywhere-cli/src/app.cpp +++ b/sdk/runanywhere-cli/src/app.cpp @@ -44,6 +44,7 @@ void configure_app(CLI::App& app, GlobalOptions& options) { commands::register_vad(app, options); commands::register_voice(app, options); commands::register_rag(app, options); + commands::register_bench(app, options); commands::register_serve(app, options); } diff --git a/sdk/runanywhere-cli/src/commands/cmd_bench.cpp b/sdk/runanywhere-cli/src/commands/cmd_bench.cpp new file mode 100644 index 0000000000..168a80b4b5 --- /dev/null +++ b/sdk/runanywhere-cli/src/commands/cmd_bench.cpp @@ -0,0 +1,741 @@ +/** + * @file cmd_bench.cpp + * @brief `rcli bench [model]` — auto-benchmark installed models, like the + * Android app's benchmark screen. + * + * With no model argument it enumerates every downloaded, non-built-in model + * from the registry and benchmarks each in its category (LLM / STT / TTS / + * VLM). Faithful port of the Android BenchmarkRunner / BenchmarkMetricPolicy + * flow: per (model, scenario), repeat `trials` times { + * unload → sample avail RAM → load (timed) → 1 warmup (discarded) + * → 1 measured pass → sample avail RAM → per-trial metrics } + * → aggregate trials by MEDIAN, report [min,max] where useful. + * + * Metrics come from the SDK result protos (LLMGenerationResult / STTOutput / + * TTSOutput / VLMResult) with wall-clock fallbacks, matching the Android + * BenchmarkMetricPolicy. No telemetry is emitted (matches Android — the + * benchmark is a pure measurement). + */ + +#include "commands/commands.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "llm_options.pb.h" +#include "llm_service.pb.h" +#include "model_types.pb.h" +#include "stt_options.pb.h" +#include "tts_options.pb.h" +#include "vlm_options.pb.h" +#include "rac/core/rac_benchmark.h" +#include "rac/core/rac_core.h" +#include "rac/core/rac_model_lifecycle.h" +#include "rac/features/llm/rac_llm_service.h" +#include "rac/features/stt/rac_stt_service.h" +#include "rac/features/tts/rac_tts_service.h" +#include "rac/features/vlm/rac_vlm_service.h" +#include "rac/infrastructure/model_management/rac_model_registry.h" + +#include "io/output.h" +#include "io/proto.h" + +namespace rcli::commands { + +namespace { + +namespace v1 = runanywhere::v1; + +// Prompts / text mirror the Android BenchmarkRunner constants so numbers are +// comparable across the CLI and the app. +constexpr const char* kLlmSystemPrompt = + "You are a helpful assistant. Always give extremely detailed, thorough responses. Never stop " + "early. Use the full response length available to you. Elaborate on every point with examples " + "and explanations."; +constexpr const char* kLlmPrompt = + "Write a very long and detailed explanation of how neural networks work, covering perceptrons, " + "activation functions, backpropagation, gradient descent, loss functions, convolutional " + "layers, recurrent layers, transformers, attention mechanisms, and training procedures. Be as " + "thorough as possible."; +constexpr const char* kVlmPrompt = "Describe this image in detail."; +constexpr const char* kTtsShort = "Hello, this is a test."; +constexpr const char* kTtsMedium = + "The quick brown fox jumps over the lazy dog. Machine learning models can generate speech from " + "text with remarkable quality and natural intonation."; +constexpr double kPi = 3.14159265358979323846; + +enum class Modality { kLlm, kStt, kTts, kVlm }; + +const char* modality_label(Modality m) { + switch (m) { + case Modality::kLlm: + return "llm"; + case Modality::kStt: + return "stt"; + case Modality::kTts: + return "tts"; + case Modality::kVlm: + return "vlm"; + } + return "?"; +} + +bool modality_of(v1::ModelCategory category, Modality* out) { + switch (category) { + case v1::MODEL_CATEGORY_LANGUAGE: + *out = Modality::kLlm; + return true; + case v1::MODEL_CATEGORY_SPEECH_RECOGNITION: + *out = Modality::kStt; + return true; + case v1::MODEL_CATEGORY_SPEECH_SYNTHESIS: + *out = Modality::kTts; + return true; + case v1::MODEL_CATEGORY_MULTIMODAL: + case v1::MODEL_CATEGORY_VISION: + *out = Modality::kVlm; + return true; + default: + return false; // vad, embedding, image-generation are not benchmarked + } +} + +struct Scenario { + const char* label; + int32_t max_tokens; // LLM/VLM + double seconds; // STT audio length + bool sine; // STT: 440 Hz tone vs silence + const char* text; // TTS input +}; + +const std::vector& scenarios_for(Modality m) { + static const std::vector llm = {{"Short (50)", 50, 0, false, nullptr}, + {"Medium (256)", 256, 0, false, nullptr}, + {"Long (512)", 512, 0, false, nullptr}}; + static const std::vector stt = {{"Silent 2s", 0, 2.0, false, nullptr}, + {"Sine Tone 3s", 0, 3.0, true, nullptr}}; + static const std::vector tts = {{"Short Text", 0, 0, false, kTtsShort}, + {"Medium Text", 0, 0, false, kTtsMedium}}; + static const std::vector vlm = {{"Image Description", 128, 0, false, nullptr}}; + switch (m) { + case Modality::kLlm: + return llm; + case Modality::kStt: + return stt; + case Modality::kTts: + return tts; + case Modality::kVlm: + return vlm; + } + return llm; +} + +// Per-trial metrics; aggregated to medians across trials. +struct Metrics { + double load_ms = 0.0; + double warmup_ms = 0.0; + double end_to_end_ms = 0.0; + double tokens_per_second = 0.0; // LLM/VLM + double prompt_eval_ms = 0.0; // LLM/VLM prefill + double decode_ms = 0.0; // LLM/VLM + int32_t output_tokens = 0; // LLM/VLM + double real_time_factor = 0.0; // STT + double chars_per_second = 0.0; // TTS + double audio_duration_ms = 0.0; // TTS + int64_t memory_delta_bytes = 0; +}; + +// --- small utilities ------------------------------------------------------- + +int64_t available_ram_bytes() { + std::FILE* f = std::fopen("/proc/meminfo", "r"); + if (!f) { + return 0; + } + char line[256]; + int64_t kb = 0; + while (std::fgets(line, sizeof(line), f)) { + if (std::sscanf(line, "MemAvailable: %lld kB", reinterpret_cast(&kb)) == 1) { + break; + } + } + std::fclose(f); + return kb * 1024; +} + +double median(std::vector values) { + std::vector v; + for (double x : values) { + if (std::isfinite(x)) { + v.push_back(x); + } + } + if (v.empty()) { + return 0.0; + } + std::sort(v.begin(), v.end()); + const size_t mid = v.size() / 2; + return (v.size() % 2 == 1) ? v[mid] : (v[mid - 1] + v[mid]) / 2.0; +} + +std::string human_bytes(int64_t bytes) { + if (bytes <= 0) { + return "-"; + } + const double b = static_cast(bytes); + char buf[32]; + if (b >= 1e9) { + std::snprintf(buf, sizeof(buf), "%.2f GB", b / 1e9); + } else if (b >= 1e6) { + std::snprintf(buf, sizeof(buf), "%.0f MB", b / 1e6); + } else { + std::snprintf(buf, sizeof(buf), "%.0f KB", b / 1e3); + } + return buf; +} + +// 16 kHz, 16-bit mono PCM: silence or a 440 Hz sine at 60% amplitude (matches +// Android SyntheticInput.silentPcm / sinePcm). +std::string make_pcm16(double seconds, bool sine) { + constexpr int kSampleRate = 16000; + const int n = static_cast(kSampleRate * seconds); + std::string out; + out.resize(static_cast(n) * 2); + auto* samples = reinterpret_cast(out.data()); + for (int i = 0; i < n; ++i) { + double v = sine ? std::sin(2.0 * kPi * 440.0 * i / kSampleRate) * 32767.0 * 0.6 : 0.0; + samples[i] = static_cast(v); + } + return out; +} + +// --- lifecycle helpers ----------------------------------------------------- + +void unload_category(v1::ModelCategory category) { + v1::ModelUnloadRequest request; + request.set_category(category); + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t out; + rac_proto_buffer_init(&out); + rac_model_lifecycle_unload_proto(reinterpret_cast(bytes.data()), bytes.size(), + &out); + rac_proto_buffer_free(&out); +} + +double load_model_timed(const std::string& model_id, v1::ModelCategory category, + std::string* out_error) { + v1::ModelLoadRequest request; + request.set_model_id(model_id); + request.set_category(category); + request.set_validate_availability(true); + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t out; + rac_proto_buffer_init(&out); + const int64_t t0 = rac_monotonic_now_ms(); + const rac_result_t rc = rac_model_lifecycle_load_proto( + rac_get_model_registry(), reinterpret_cast(bytes.data()), bytes.size(), + &out); + const int64_t t1 = rac_monotonic_now_ms(); + v1::ModelLoadResult result; + std::string parse_err; + if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&out, &result, &parse_err)) { + *out_error = parse_err.empty() ? "load failed" : parse_err; + return -1.0; + } + if (!result.success()) { + *out_error = result.error_message().empty() ? "load failed" : result.error_message(); + return -1.0; + } + return static_cast(t1 - t0); +} + +// --- per-modality inference calls ------------------------------------------ + +bool llm_generate(int32_t max_tokens, bool system_prompt, v1::LLMGenerationResult* out, + std::string* err) { + v1::LLMGenerateRequest request; + request.set_prompt(kLlmPrompt); + v1::LLMGenerationOptions* gen = request.mutable_options(); + gen->set_max_tokens(max_tokens); + gen->set_temperature(0.0f); + if (system_prompt) { + gen->set_system_prompt(kLlmSystemPrompt); + } + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t buf; + rac_proto_buffer_init(&buf); + const rac_result_t rc = + rac_llm_generate_proto(reinterpret_cast(bytes.data()), bytes.size(), &buf); + if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { + if (err->empty()) { + *err = rac_error_message(rc); + } + return false; + } + return true; +} + +bool stt_transcribe(const std::string& pcm, v1::STTOutput* out, std::string* err) { + v1::STTTranscriptionRequest request; + v1::STTAudioSource* audio = request.mutable_audio(); + audio->set_audio_data(pcm); + audio->set_encoding(v1::STT_AUDIO_ENCODING_PCM_S16_LE); + audio->set_sample_rate(16000); + audio->set_channels(1); + audio->set_bits_per_sample(16); + v1::STTOptions* opts = request.mutable_options(); + opts->set_language(v1::STT_LANGUAGE_EN); + opts->set_sample_rate(16000); + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t buf; + rac_proto_buffer_init(&buf); + const rac_result_t rc = rac_stt_transcribe_lifecycle_proto( + reinterpret_cast(bytes.data()), bytes.size(), &buf); + if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { + if (err->empty()) { + *err = rac_error_message(rc); + } + return false; + } + return true; +} + +bool tts_synthesize(const std::string& text, v1::TTSOutput* out, std::string* err) { + v1::TTSSynthesisRequest request; + request.set_text(text); + v1::TTSOptions* opts = request.mutable_options(); + opts->set_sample_rate(22050); + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t buf; + rac_proto_buffer_init(&buf); + const rac_result_t rc = rac_tts_synthesize_lifecycle_proto( + reinterpret_cast(bytes.data()), bytes.size(), &buf); + if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { + if (err->empty()) { + *err = rac_error_message(rc); + } + return false; + } + return true; +} + +bool vlm_process(const std::string& image_path, int32_t max_tokens, v1::VLMResult* out, + std::string* err) { + v1::VLMGenerationRequest request; + v1::VLMImage* image = request.add_images(); + image->set_file_path(image_path); + v1::VLMGenerationOptions* gen = request.mutable_options(); + gen->set_prompt(kVlmPrompt); + gen->set_max_tokens(max_tokens); + gen->set_temperature(0.0f); + const std::string bytes = proto::serialize(request); + rac_proto_buffer_t buf; + rac_proto_buffer_init(&buf); + const rac_result_t rc = + rac_vlm_generate_proto(reinterpret_cast(bytes.data()), bytes.size(), &buf); + if (rc != RAC_SUCCESS || !proto::parse_proto_buffer(&buf, out, err)) { + if (err->empty()) { + *err = rac_error_message(rc); + } + return false; + } + return true; +} + +// --- per-trial runners (one load → warmup → measured pass) ----------------- + +struct TrialCtx { + std::string model_id; + v1::ModelCategory category; + Scenario scenario; + std::string vlm_image; +}; + +bool llm_trial(const TrialCtx& c, Metrics* m, std::string* err) { + unload_category(c.category); + const int64_t mem_before = available_ram_bytes(); + m->load_ms = load_model_timed(c.model_id, c.category, err); + if (m->load_ms < 0.0) { + return false; + } + const int64_t w0 = rac_monotonic_now_ms(); + v1::LLMGenerationResult warm; + if (!llm_generate(5, false, &warm, err)) { + unload_category(c.category); + return false; + } + m->warmup_ms = static_cast(rac_monotonic_now_ms() - w0); + + const int64_t t0 = rac_monotonic_now_ms(); + v1::LLMGenerationResult r; + if (!llm_generate(c.scenario.max_tokens, true, &r, err)) { + unload_category(c.category); + return false; + } + const double measured_e2e = static_cast(rac_monotonic_now_ms() - t0); + m->memory_delta_bytes = mem_before - available_ram_bytes(); + unload_category(c.category); + + const int32_t out_tokens = r.tokens_generated(); + if (out_tokens <= 0) { + *err = "no output tokens"; + return false; + } + const double e2e = r.generation_time_ms() > 0.0 ? r.generation_time_ms() : measured_e2e; + const double explicit_decode = + r.decode_time_ms() > 0 ? static_cast(r.decode_time_ms()) : 0.0; + double tps = r.tokens_per_second() > 0.0 ? r.tokens_per_second() : 0.0; + if (tps <= 0.0 && explicit_decode > 0.0) { + tps = out_tokens * 1000.0 / explicit_decode; + } + if (tps <= 0.0 && e2e > 0.0) { + tps = out_tokens * 1000.0 / e2e; + } + m->end_to_end_ms = e2e; + m->tokens_per_second = tps; + m->decode_ms = explicit_decode > 0.0 ? explicit_decode : (tps > 0.0 ? out_tokens * 1000.0 / tps + : 0.0); + m->prompt_eval_ms = r.prompt_eval_time_ms() > 0 ? static_cast(r.prompt_eval_time_ms()) + : (r.has_ttft_ms() ? r.ttft_ms() : 0.0); + m->output_tokens = out_tokens; + return true; +} + +bool stt_trial(const TrialCtx& c, Metrics* m, std::string* err) { + unload_category(c.category); + const int64_t mem_before = available_ram_bytes(); + m->load_ms = load_model_timed(c.model_id, c.category, err); + if (m->load_ms < 0.0) { + return false; + } + v1::STTOutput warm; + (void)stt_transcribe(make_pcm16(0.5, false), &warm, err); // warmup, errors ignored + + const int64_t t0 = rac_monotonic_now_ms(); + v1::STTOutput r; + if (!stt_transcribe(make_pcm16(c.scenario.seconds, c.scenario.sine), &r, err)) { + unload_category(c.category); + return false; + } + m->end_to_end_ms = static_cast(rac_monotonic_now_ms() - t0); + m->memory_delta_bytes = mem_before - available_ram_bytes(); + unload_category(c.category); + + m->real_time_factor = r.has_metadata() && r.metadata().real_time_factor() > 0.0 + ? r.metadata().real_time_factor() + : (c.scenario.seconds > 0.0 + ? m->end_to_end_ms / (c.scenario.seconds * 1000.0) + : 0.0); + return true; +} + +bool tts_trial(const TrialCtx& c, Metrics* m, std::string* err) { + unload_category(c.category); + const int64_t mem_before = available_ram_bytes(); + m->load_ms = load_model_timed(c.model_id, c.category, err); + if (m->load_ms < 0.0) { + return false; + } + v1::TTSOutput warm; + (void)tts_synthesize("Hi.", &warm, err); // warmup, errors ignored + + const std::string text = c.scenario.text ? c.scenario.text : ""; + const int64_t t0 = rac_monotonic_now_ms(); + v1::TTSOutput r; + if (!tts_synthesize(text, &r, err)) { + unload_category(c.category); + return false; + } + m->end_to_end_ms = static_cast(rac_monotonic_now_ms() - t0); + m->memory_delta_bytes = mem_before - available_ram_bytes(); + unload_category(c.category); + + m->audio_duration_ms = static_cast(r.duration_ms()); + const int32_t chars = r.has_metadata() && r.metadata().character_count() > 0 + ? r.metadata().character_count() + : static_cast(text.size()); + m->chars_per_second = m->end_to_end_ms > 0.0 ? chars * 1000.0 / m->end_to_end_ms : 0.0; + return true; +} + +bool vlm_trial(const TrialCtx& c, Metrics* m, std::string* err) { + unload_category(v1::MODEL_CATEGORY_MULTIMODAL); + unload_category(v1::MODEL_CATEGORY_LANGUAGE); + const int64_t mem_before = available_ram_bytes(); + m->load_ms = load_model_timed(c.model_id, c.category, err); + if (m->load_ms < 0.0) { + return false; + } + v1::VLMResult warm; + (void)vlm_process(c.vlm_image, 1, &warm, err); // warmup, errors ignored + + const int64_t t0 = rac_monotonic_now_ms(); + v1::VLMResult r; + if (!vlm_process(c.vlm_image, c.scenario.max_tokens, &r, err)) { + unload_category(c.category); + return false; + } + const double measured_e2e = static_cast(rac_monotonic_now_ms() - t0); + m->memory_delta_bytes = mem_before - available_ram_bytes(); + unload_category(c.category); + + const int32_t out_tokens = r.completion_tokens(); + m->end_to_end_ms = r.processing_time_ms() > 0 ? static_cast(r.processing_time_ms()) + : measured_e2e; + m->tokens_per_second = r.tokens_per_second(); + m->prompt_eval_ms = static_cast(r.time_to_first_token_ms()); + m->output_tokens = out_tokens; + if (m->tokens_per_second <= 0.0 && out_tokens > 0 && m->end_to_end_ms > 0.0) { + m->tokens_per_second = out_tokens * 1000.0 / m->end_to_end_ms; + } + m->decode_ms = m->tokens_per_second > 0.0 ? out_tokens * 1000.0 / m->tokens_per_second : 0.0; + return true; +} + +// --- aggregation + report -------------------------------------------------- + +struct BenchRow { + std::string model_id; + Modality modality; + std::string scenario; + bool success = false; + std::string error; + int trials = 0; + Metrics med; +}; + +using TrialFn = std::function; + +BenchRow aggregate(const GlobalOptions& options, const TrialCtx& ctx, Modality modality, int trials, + const TrialFn& trial) { + BenchRow row; + row.model_id = ctx.model_id; + row.modality = modality; + row.scenario = ctx.scenario.label; + row.trials = trials; + + std::vector load, warmup, e2e, tps, prefill, decode, mem, rtf, cps, adur; + std::vector out_tok; + for (int t = 0; t < trials; ++t) { + Metrics m; + std::string err; + if (!trial(ctx, &m, &err)) { + row.error = err; + return row; + } + load.push_back(m.load_ms); + warmup.push_back(m.warmup_ms); + e2e.push_back(m.end_to_end_ms); + tps.push_back(m.tokens_per_second); + prefill.push_back(m.prompt_eval_ms); + decode.push_back(m.decode_ms); + mem.push_back(static_cast(m.memory_delta_bytes)); + rtf.push_back(m.real_time_factor); + cps.push_back(m.chars_per_second); + adur.push_back(m.audio_duration_ms); + out_tok.push_back(m.output_tokens); + if (options.verbose) { + out::status_line(" trial " + std::to_string(t + 1) + "/" + std::to_string(trials) + + " ok"); + } + } + row.success = true; + row.med.load_ms = median(load); + row.med.warmup_ms = median(warmup); + row.med.end_to_end_ms = median(e2e); + row.med.tokens_per_second = median(tps); + row.med.prompt_eval_ms = median(prefill); + row.med.decode_ms = median(decode); + row.med.memory_delta_bytes = static_cast(median(mem)); + row.med.real_time_factor = median(rtf); + row.med.chars_per_second = median(cps); + row.med.audio_duration_ms = median(adur); + row.med.output_tokens = out_tok.empty() ? 0 : out_tok[out_tok.size() / 2]; + return row; +} + +// Modality-specific "primary" throughput/latency string for the report. +std::string primary_metric(const BenchRow& r) { + char buf[64]; + switch (r.modality) { + case Modality::kLlm: + case Modality::kVlm: + std::snprintf(buf, sizeof(buf), "%.1f tok/s %.0fms pf", r.med.tokens_per_second, + r.med.prompt_eval_ms); + break; + case Modality::kStt: + std::snprintf(buf, sizeof(buf), "RTF %.3f (%.0fx rt)", r.med.real_time_factor, + r.med.real_time_factor > 0.0 ? 1.0 / r.med.real_time_factor : 0.0); + break; + case Modality::kTts: + std::snprintf(buf, sizeof(buf), "%.0f chars/s", r.med.chars_per_second); + break; + } + return buf; +} + +// --- enumeration + driver -------------------------------------------------- + +struct BenchModel { + std::string id; + v1::ModelCategory category; + Modality modality; +}; + +bool collect_models(const std::string& only_model, std::vector* out, + std::string* out_error) { + rac_proto_buffer_t buf; + rac_proto_buffer_init(&buf); + if (rac_model_registry_list_downloaded_proto_buffer(rac_get_model_registry(), &buf) != + RAC_SUCCESS) { + *out_error = "failed to list downloaded models"; + return false; + } + v1::ModelInfoList list; + if (!proto::parse_proto_buffer(&buf, &list, out_error)) { + return false; + } + for (const v1::ModelInfo& m : list.models()) { + if (!only_model.empty() && m.id() != only_model) { + continue; + } + const bool builtin = m.framework() == v1::INFERENCE_FRAMEWORK_FOUNDATION_MODELS || + m.framework() == v1::INFERENCE_FRAMEWORK_SYSTEM_TTS; + if (builtin) { + continue; + } + Modality modality; + if (!modality_of(m.category(), &modality)) { + continue; + } + out->push_back({m.id(), m.category(), modality}); + } + return true; +} + +int run_bench(const GlobalOptions& options, const std::string& only_model, int trials, + const std::string& vlm_image) { + Bootstrapped env; + if (bootstrap(options, &env) != RAC_SUCCESS) { + return 1; + } + if (trials < 1) { + trials = 1; + } + + std::vector models; + std::string error; + if (!collect_models(only_model, &models, &error)) { + out::error_line(error); + return 1; + } + if (models.empty()) { + out::error_line(only_model.empty() + ? "no downloaded models to benchmark (pull one with `rcli pull`)" + : "model '" + only_model + "' is not a downloaded benchmarkable model"); + return 1; + } + + std::vector rows; + for (const BenchModel& model : models) { + for (const Scenario& scenario : scenarios_for(model.modality)) { + out::status_line(std::string("benchmarking ") + modality_label(model.modality) + " " + + model.id + " — " + scenario.label + " (" + std::to_string(trials) + + " trials)"); + TrialCtx ctx{model.id, model.category, scenario, vlm_image}; + TrialFn fn; + switch (model.modality) { + case Modality::kLlm: + fn = llm_trial; + break; + case Modality::kStt: + fn = stt_trial; + break; + case Modality::kTts: + fn = tts_trial; + break; + case Modality::kVlm: + fn = vlm_trial; + break; + } + rows.push_back(aggregate(options, ctx, model.modality, trials, fn)); + } + } + + if (options.json) { + out::JsonWriter json; + json.begin_object().begin_array("results"); + for (const BenchRow& r : rows) { + json.begin_array_object() + .field("model", r.model_id) + .field("modality", modality_label(r.modality)) + .field("scenario", r.scenario) + .field("success", r.success) + .field("trials", static_cast(r.trials)); + if (r.success) { + json.field("tokens_per_second", r.med.tokens_per_second) + .field("prompt_eval_ms", r.med.prompt_eval_ms) + .field("decode_ms", r.med.decode_ms) + .field("end_to_end_ms", r.med.end_to_end_ms) + .field("real_time_factor", r.med.real_time_factor) + .field("chars_per_second", r.med.chars_per_second) + .field("output_tokens", static_cast(r.med.output_tokens)) + .field("load_ms", r.med.load_ms) + .field("memory_delta_bytes", r.med.memory_delta_bytes); + } else { + json.field("error", r.error); + } + json.end_object(); + } + json.end_array().end_object(); + out::result_line(json.str()); + return 0; + } + + out::result_line(""); + out::result_line( + "MODEL MOD SCENARIO PRIMARY LOAD MEMΔ"); + for (const BenchRow& r : rows) { + char line[256]; + if (r.success) { + std::snprintf(line, sizeof(line), "%-30.30s %-4.4s %-15.15s %-22.22s %6.0fms %s", + r.model_id.c_str(), modality_label(r.modality), r.scenario.c_str(), + primary_metric(r).c_str(), r.med.load_ms, + human_bytes(r.med.memory_delta_bytes).c_str()); + } else { + std::snprintf(line, sizeof(line), "%-30.30s %-4.4s %-15.15s FAILED: %s", + r.model_id.c_str(), modality_label(r.modality), r.scenario.c_str(), + r.error.c_str()); + } + out::result_line(line); + } + return 0; +} + +} // namespace + +void register_bench(CLI::App& app, GlobalOptions& options) { + CLI::App* cmd = app.add_subcommand( + "bench", "Benchmark installed models (auto-runs all downloaded LLM/STT/TTS/VLM models)"); + auto model = std::make_shared(); + auto trials = std::make_shared(3); + auto vlm_image = std::make_shared("docs/gifs/npu-model-tag-screenshot.png"); + cmd->add_option("model", *model, "Model id to benchmark (default: all downloaded models)"); + cmd->add_option("--trials,-n", *trials, "Measured trials per scenario (median reported)") + ->default_val(3); + cmd->add_option("--vlm-image", *vlm_image, "Image file for VLM benchmarking"); + cmd->callback([&options, model, trials, vlm_image]() { + const int exit_code = run_bench(options, *model, *trials, *vlm_image); + if (exit_code != 0) { + throw CLI::RuntimeError(exit_code); + } + }); +} + +} // namespace rcli::commands diff --git a/sdk/runanywhere-cli/src/commands/commands.h b/sdk/runanywhere-cli/src/commands/commands.h index d8b47c446a..473f85ac98 100644 --- a/sdk/runanywhere-cli/src/commands/commands.h +++ b/sdk/runanywhere-cli/src/commands/commands.h @@ -37,6 +37,7 @@ void register_voice(CLI::App& app, GlobalOptions& options); void register_serve(CLI::App& app, GlobalOptions& options); void register_lora(CLI::App& app, GlobalOptions& options); void register_rag(CLI::App& app, GlobalOptions& options); +void register_bench(CLI::App& app, GlobalOptions& options); /** * Shared pull flow (plan → start → progress → terminal state) for an diff --git a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp index 36fa6f22bc..cd937de675 100644 --- a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp +++ b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp @@ -1253,6 +1253,12 @@ using runanywhere::v1::LLMStreamFinalResult; using runanywhere::v1::SDKEvent; using runanywhere::v1::TokenKind; +// Defined below; replaces invalid UTF-8 with U+FFFD so model output is safe to +// store in proto `string` fields (llama.cpp can cut a multibyte char at +// max_tokens). Forward-declared so the emit helpers above its definition can +// use it. +std::string sanitize_utf8(const std::string& in); + int64_t now_ms() { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); @@ -1330,7 +1336,7 @@ void publish_generation_event(GenerationEventKind kind, const char* prompt, cons generation->set_token(token); } if ((response != nullptr) && response[0] != '\0') { - generation->set_response(response); + generation->set_response(sanitize_utf8(response)); } if ((error != nullptr) && error[0] != '\0') { generation->set_error(error); @@ -1563,13 +1569,53 @@ options_from_request(const LLMGenerateRequest& request, const std::string& syste return options; } +// Replace any invalid UTF-8 byte sequence with U+FFFD so the value is safe to +// store in a proto `string` field. llama.cpp can emit an incomplete trailing +// multibyte sequence when generation is cut at max_tokens; without this, +// protobuf serialization of LLMGenerationResult.text fails and the whole +// unary result is unparseable by the caller. +std::string sanitize_utf8(const std::string& in) { + std::string out; + out.reserve(in.size()); + const size_t n = in.size(); + size_t i = 0; + auto is_cont = [&](size_t k) { + return k < n && (static_cast(in[k]) & 0xC0) == 0x80; + }; + while (i < n) { + const unsigned char c = static_cast(in[i]); + size_t len = 0; + if (c < 0x80) { + len = 1; + } else if ((c & 0xE0) == 0xC0 && c >= 0xC2) { + len = 2; + } else if ((c & 0xF0) == 0xE0) { + len = 3; + } else if ((c & 0xF8) == 0xF0 && c <= 0xF4) { + len = 4; + } + bool ok = len > 0; + for (size_t k = 1; ok && k < len; ++k) { + ok = is_cont(i + k); + } + if (ok) { + out.append(in, i, len); + i += len; + } else { + out.append("\xEF\xBF\xBD"); // U+FFFD replacement character + i += 1; + } + } + return out; +} + void set_result_from_raw(const rac::llm::LifecycleLlmRef& ref, const rac_llm_result_t& raw, const char* response, size_t response_len, const char* thinking, size_t thinking_len, int32_t thinking_tokens, int32_t response_tokens, int32_t requested_max_tokens, LLMGenerationResult* out) { - out->set_text(response ? std::string(response, response_len) : std::string()); + out->set_text(sanitize_utf8(response ? std::string(response, response_len) : std::string())); if (thinking && thinking_len > 0) { - out->set_thinking_content(std::string(thinking, thinking_len)); + out->set_thinking_content(sanitize_utf8(std::string(thinking, thinking_len))); } out->set_input_tokens(raw.prompt_tokens); out->set_tokens_generated(raw.completion_tokens); @@ -1579,6 +1625,9 @@ void set_result_from_raw(const rac::llm::LifecycleLlmRef& ref, const rac_llm_res if (raw.time_to_first_token_ms > 0) { out->set_ttft_ms(static_cast(raw.time_to_first_token_ms)); } + if (raw.prompt_eval_time_ms > 0) { + out->set_prompt_eval_time_ms(raw.prompt_eval_time_ms); + } out->set_tokens_per_second(static_cast(raw.tokens_per_second)); if ((ref.framework_name != nullptr) && ref.framework_name[0] != '\0') { out->set_framework(ref.framework_name); @@ -1617,13 +1666,13 @@ void set_structured_output_if_present(const char* response, LLMGenerationResult* auto* structured = out->mutable_structured_output_validation(); structured->set_is_valid(true); structured->set_contains_json(true); - structured->set_raw_output(response); + structured->set_raw_output(sanitize_utf8(response)); structured->set_extracted_json(validation.extracted_json); } else if (validation.error_message) { auto* structured = out->mutable_structured_output_validation(); structured->set_is_valid(false); structured->set_contains_json(false); - structured->set_raw_output(response); + structured->set_raw_output(sanitize_utf8(response)); structured->set_error_message(validation.error_message); } } From 77f518c4e67db06fec288eaf3d5e97dab8510793 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 08:36:10 +0530 Subject: [PATCH 05/44] Fix LLM token undercount: use decode-loop count, not streaming-callback flushes --- engines/llamacpp/llamacpp_backend.cpp | 16 ++++++++++++---- engines/llamacpp/llamacpp_backend.h | 7 ++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/engines/llamacpp/llamacpp_backend.cpp b/engines/llamacpp/llamacpp_backend.cpp index 34ae2c1403..fe5f0b8600 100644 --- a/engines/llamacpp/llamacpp_backend.cpp +++ b/engines/llamacpp/llamacpp_backend.cpp @@ -818,7 +818,8 @@ TextGenerationResult LlamaCppTextGeneration::generate(const TextGenerationReques result.finish_reason = "error"; std::string generated_text; - int tokens_generated = 0; + int callback_pieces = 0; + int decoded_tokens = 0; int prompt_tokens = 0; double prompt_eval_ms = 0.0; @@ -829,10 +830,14 @@ TextGenerationResult LlamaCppTextGeneration::generate(const TextGenerationReques request, [&](const std::string& token) -> bool { generated_text += token; - tokens_generated++; + callback_pieces++; return !cancel_requested_.load(); }, - &prompt_tokens, &prompt_eval_ms); + &prompt_tokens, &prompt_eval_ms, &decoded_tokens); + // The streaming callback flushes buffered chunks, not one call per token, so + // callback_pieces under-counts. Use the decode loop's authoritative count; + // fall back to the piece count only if the out-param wasn't populated. + const int tokens_generated = decoded_tokens > 0 ? decoded_tokens : callback_pieces; RAC_LOG_INFO("LLM.LlamaCpp", "generate(): generate_stream returned success=%d, tokens=%d", success, tokens_generated); @@ -975,7 +980,7 @@ int LlamaCppTextGeneration::run_decode_loop(llama_sampler* sampler, llama_batch& bool LlamaCppTextGeneration::generate_stream(const TextGenerationRequest& request, TextStreamCallback callback, int* out_prompt_tokens, - double* out_prompt_eval_ms) { + double* out_prompt_eval_ms, int* out_tokens_generated) { std::lock_guard lock(mutex_); if (!is_ready_locked()) { @@ -1158,6 +1163,9 @@ bool LlamaCppTextGeneration::generate_stream(const TextGenerationRequest& reques // generate_from_context() via run_decode_loop(). const int tokens_generated = run_decode_loop(sampler_, batch, batch.n_tokens, effective_max_tokens, callback); + if (out_tokens_generated != nullptr) { + *out_tokens_generated = tokens_generated; + } // TODO(streaming-tools): Emit tool_call_delta events during stream. // To support generateWithToolsStream for Web and RN, the generate_stream diff --git a/engines/llamacpp/llamacpp_backend.h b/engines/llamacpp/llamacpp_backend.h index cac50fb12d..e46214f62f 100644 --- a/engines/llamacpp/llamacpp_backend.h +++ b/engines/llamacpp/llamacpp_backend.h @@ -154,9 +154,14 @@ class LlamaCppTextGeneration { * @param callback Streaming callback; return false to cancel. * @param out_prompt_tokens Optional: tokenized prompt length (may be NULL). * @param out_prompt_eval_ms Optional: prefill (prompt decode) time in ms (may be NULL). + * @param out_tokens_generated Optional: authoritative decoded-token count from + * the decode loop (may be NULL). Prefer this over counting streaming + * callback invocations — the callback flushes buffered chunks, not one + * call per token, so callback counts under-report generated tokens. */ bool generate_stream(const TextGenerationRequest& request, TextStreamCallback callback, - int* out_prompt_tokens = nullptr, double* out_prompt_eval_ms = nullptr); + int* out_prompt_tokens = nullptr, double* out_prompt_eval_ms = nullptr, + int* out_tokens_generated = nullptr); void cancel(); From 607cb638734820db1bbda1c685ca0a9957839231 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 08:54:22 +0530 Subject: [PATCH 06/44] Fix LLM streaming token undercount: thread authoritative decode count through vtable generate_stream --- .../llamacpp/rac_backend_llamacpp_register.cpp | 5 +++-- engines/llamacpp/rac_llm_llamacpp.cpp | 15 ++++++++++++--- engines/mlx/rac_mlx_engine.cpp | 7 ++++++- .../include/rac/backends/rac_llm_llamacpp.h | 3 ++- .../include/rac/features/llm/rac_llm_service.h | 12 ++++++++++-- .../src/features/llm/llm_module.cpp | 16 +++++++++++----- .../src/features/llm/rac_llm_service.cpp | 2 +- .../src/features/llm/structured_output.cpp | 3 ++- 8 files changed, 47 insertions(+), 16 deletions(-) diff --git a/engines/llamacpp/rac_backend_llamacpp_register.cpp b/engines/llamacpp/rac_backend_llamacpp_register.cpp index 76f302b5e4..d3f190cc3d 100644 --- a/engines/llamacpp/rac_backend_llamacpp_register.cpp +++ b/engines/llamacpp/rac_backend_llamacpp_register.cpp @@ -176,10 +176,11 @@ static rac_bool_t stream_adapter_callback(const char* token, rac_bool_t is_final static rac_result_t llamacpp_vtable_generate_stream(void* impl, const char* prompt, const rac_llm_options_t* options, rac_llm_stream_callback_fn callback, - void* user_data) { + void* user_data, + int32_t* out_tokens_generated) { StreamAdapter adapter = {callback, user_data}; return rac_llm_llamacpp_generate_stream(legacy_handle(impl), prompt, options, - stream_adapter_callback, &adapter); + stream_adapter_callback, &adapter, out_tokens_generated); } // Get info diff --git a/engines/llamacpp/rac_llm_llamacpp.cpp b/engines/llamacpp/rac_llm_llamacpp.cpp index ce570e7081..db3a2e0ccb 100644 --- a/engines/llamacpp/rac_llm_llamacpp.cpp +++ b/engines/llamacpp/rac_llm_llamacpp.cpp @@ -353,7 +353,10 @@ rac_result_t rac_llm_llamacpp_generate(rac_handle_t handle, const char* prompt, rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* prompt, const rac_llm_options_t* options, rac_llm_llamacpp_stream_callback_fn callback, - void* user_data) { + void* user_data, int32_t* out_tokens_generated) { + if (out_tokens_generated != nullptr) { + *out_tokens_generated = 0; + } if (handle == nullptr || prompt == nullptr || callback == nullptr) { return RAC_ERROR_NULL_POINTER; } @@ -421,9 +424,11 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p bool terminal_emitted = false; bool success = false; + int decoded_tokens = 0; try { success = h->text_gen->generate_stream( - request, [&](const std::string& token) -> bool { + request, + [&](const std::string& token) -> bool { if (user_stops.empty()) { return callback(token.c_str(), RAC_FALSE, user_data) == RAC_TRUE; } @@ -445,7 +450,8 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p return callback(safe_chunk.c_str(), RAC_FALSE, user_data) == RAC_TRUE; } return true; - }); + }, + /*out_prompt_tokens=*/nullptr, /*out_prompt_eval_ms=*/nullptr, &decoded_tokens); } catch (const std::exception& e) { rac_error_set_details(e.what()); if (!terminal_emitted) { @@ -474,6 +480,9 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p } callback("", RAC_TRUE, user_data); // Final token terminal_emitted = true; + if (out_tokens_generated != nullptr) { + *out_tokens_generated = decoded_tokens; + } return RAC_SUCCESS; } diff --git a/engines/mlx/rac_mlx_engine.cpp b/engines/mlx/rac_mlx_engine.cpp index c831b7cf5a..5fd63c9da1 100644 --- a/engines/mlx/rac_mlx_engine.cpp +++ b/engines/mlx/rac_mlx_engine.cpp @@ -328,7 +328,12 @@ rac_result_t llm_generate(void* impl, const char* prompt, const rac_llm_options_ } rac_result_t llm_generate_stream(void* impl, const char* prompt, const rac_llm_options_t* options, - rac_llm_stream_callback_fn callback, void* user_data) { + rac_llm_stream_callback_fn callback, void* user_data, + int32_t* out_tokens_generated) { + // The MLX Swift callback path does not surface a decoded-token count here; + // leave out_tokens_generated untouched so callers fall back to the streaming + // callback count. + (void)out_tokens_generated; if (!prompt || !callback) { return RAC_ERROR_NULL_POINTER; } diff --git a/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h b/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h index f6b352ff27..1dc106088a 100644 --- a/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h +++ b/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h @@ -148,7 +148,8 @@ typedef rac_bool_t (*rac_llm_llamacpp_stream_callback_fn)(const char* token, rac */ RAC_LLAMACPP_API rac_result_t rac_llm_llamacpp_generate_stream( rac_handle_t handle, const char* prompt, const rac_llm_options_t* options, - rac_llm_llamacpp_stream_callback_fn callback, void* user_data); + rac_llm_llamacpp_stream_callback_fn callback, void* user_data, + int32_t* out_tokens_generated); /** * Cancels ongoing generation. diff --git a/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h b/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h index ed718edb74..e422d4473e 100644 --- a/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h +++ b/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h @@ -50,10 +50,18 @@ typedef struct rac_llm_service_ops { rac_result_t (*generate)(void* impl, const char* prompt, const rac_llm_options_t* options, rac_llm_result_t* out_result); - /** Generate text with streaming callback */ + /** Generate text with streaming callback. + * + * out_tokens_generated (optional, may be NULL): receives the backend's + * authoritative decoded-token count. Prefer this over counting streaming + * callback invocations — the callback delivers buffered chunks, not one call + * per token, so callback counts under-report generated tokens. A backend + * that does not populate it leaves it untouched; callers fall back to the + * callback count. */ rac_result_t (*generate_stream)(void* impl, const char* prompt, const rac_llm_options_t* options, - rac_llm_stream_callback_fn callback, void* user_data); + rac_llm_stream_callback_fn callback, void* user_data, + int32_t* out_tokens_generated); /** Get service info */ rac_result_t (*get_info)(void* impl, rac_llm_info_t* out_info); diff --git a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp index cd937de675..7950826398 100644 --- a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp +++ b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp @@ -2253,9 +2253,10 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, // SDKs it would be undefined behaviour through a C ABI return. const std::string effective_prompt = rac::llm::apply_no_think_directive(request.prompt(), options.disable_thinking); + int32_t backend_tokens_generated = 0; try { rc = ref.ops->generate_stream(ref.impl, effective_prompt.c_str(), &options, - stream_token_callback, &ctx); + stream_token_callback, &ctx, &backend_tokens_generated); } catch (const std::exception& e) { rac_error_set_details(e.what()); rc = RAC_ERROR_INFERENCE_FAILED; @@ -2288,8 +2289,13 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, // streaming proto generation looks like a natural stop, which breaks // OpenAI parity for direct streaming proto callers (JNI, Web, etc.) // and diverges from the non-streaming proto path. + // Prefer the backend's authoritative decoded-token count; the streaming + // callback fires per buffered chunk, so ctx.token_count (callback count) + // under-reports. Fall back to it only when the backend didn't populate. + const int64_t final_tokens = + backend_tokens_generated > 0 ? backend_tokens_generated : ctx.token_count; const char* finish_reason = - (options.max_tokens > 0 && ctx.token_count >= options.max_tokens) ? "length" : "stop"; + (options.max_tokens > 0 && final_tokens >= options.max_tokens) ? "length" : "stop"; dispatch_terminal_once(&ctx, finish_reason, nullptr); const int64_t stream_elapsed = now_ms() - ctx.started_ms; // Tokens/sec over decode time only, not prefill-inclusive wall time. @@ -2300,10 +2306,10 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, : stream_elapsed; publish_generation_event(runanywhere::v1::GENERATION_EVENT_KIND_STREAM_COMPLETED, request.prompt().c_str(), nullptr, ctx.response_text.c_str(), - nullptr, ref.model_id, ctx.token_count, stream_elapsed, + nullptr, ref.model_id, final_tokens, stream_elapsed, ctx.prompt_tokens, ref.framework_name, - (ctx.token_count > 0 && stream_decode > 0) - ? ctx.token_count * 1000.0 / static_cast(stream_decode) + (final_tokens > 0 && stream_decode > 0) + ? final_tokens * 1000.0 / static_cast(stream_decode) : 0.0, static_cast(stream_ttft), options.temperature, options.max_tokens, lifecycle_context_length(ref), diff --git a/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp b/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp index 1af7dc93d6..f3a84ec3e9 100644 --- a/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp +++ b/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp @@ -148,7 +148,7 @@ rac_result_t rac_llm_generate_stream(rac_handle_t handle, const char* prompt, const std::string effective_prompt = rac::llm::apply_no_think_directive(prompt, options ? options->disable_thinking : RAC_FALSE); return service->ops->generate_stream(service->impl, effective_prompt.c_str(), options, callback, - user_data); + user_data, /*out_tokens_generated=*/nullptr); } rac_result_t rac_llm_get_info(rac_handle_t handle, rac_llm_info_t* out_info) { diff --git a/sdk/runanywhere-commons/src/features/llm/structured_output.cpp b/sdk/runanywhere-commons/src/features/llm/structured_output.cpp index ac9ae74761..ceb53c570b 100644 --- a/sdk/runanywhere-commons/src/features/llm/structured_output.cpp +++ b/sdk/runanywhere-commons/src/features/llm/structured_output.cpp @@ -1555,7 +1555,8 @@ rac_structured_output_generate_stream_proto(const uint8_t* request_proto_bytes, // rac_llm_proto_service.cpp generate_stream path. try { rc = ref.ops->generate_stream(ref.impl, prepared_prompt.c_str(), &options, - structured_stream_token_callback, &ctx); + structured_stream_token_callback, &ctx, + /*out_tokens_generated=*/nullptr); } catch (const std::exception& e) { rac_error_set_details(e.what()); rc = RAC_ERROR_INFERENCE_FAILED; From 408a0340141bc06c88ab1a2304b7b3536d54dc9c Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 08:59:04 +0530 Subject: [PATCH 07/44] Chat composer redesign: two-row layout with thinking toggle (from #560) --- .../ui/screens/chat/ChatInputBar.kt | 132 +++++++++++++----- .../ui/screens/chat/ChatScreen.kt | 3 + .../ui/screens/chat/ChatViewModel.kt | 10 ++ 3 files changed, 113 insertions(+), 32 deletions(-) diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatInputBar.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatInputBar.kt index 1c66896980..a1991456a2 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatInputBar.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatInputBar.kt @@ -6,11 +6,12 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding @@ -35,6 +36,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.semantics.contentDescription @@ -62,14 +64,14 @@ private data class AttachmentAction( @Composable fun ChatInputBar( - input: String, - onInputChange: (String) -> Unit, - onSend: () -> Unit, - canSend: Boolean, - isGenerating: Boolean, - isStopping: Boolean, - onStop: () -> Unit, - toolsEnabled: Boolean, + input: String = "", + onInputChange: (String) -> Unit = {}, + onSend: () -> Unit = {}, + canSend: Boolean = false, + isGenerating: Boolean = false, + isStopping: Boolean = false, + onStop: () -> Unit = {}, + toolsEnabled: Boolean = false, toolsUnavailableMessage: String?, onToggleTools: () -> Unit, onAttachDocument: () -> Unit, @@ -77,6 +79,9 @@ fun ChatInputBar( onOpenLive: () -> Unit, onOpenTalk: () -> Unit, onOpenAdvanced: () -> Unit, + onToggleThinking: () -> Unit, + thinkingEnabled: Boolean, + thinkingSupported: Boolean, modifier: Modifier = Modifier, pendingAttachment: ComposerAttachment? = null, onClearAttachment: () -> Unit = {}, @@ -85,10 +90,25 @@ fun ChatInputBar( val dimens = LocalDimens.current var menuExpanded by remember { mutableStateOf(false) } val actions = listOf( - AttachmentAction("Document", "Ask questions with sources", RACIcons.Outline.FileText, onAttachDocument), + AttachmentAction( + "Document", + "Ask questions with sources", + RACIcons.Outline.FileText, + onAttachDocument + ), AttachmentAction("Image", "Ask about a photo", RACIcons.Outline.Eye, onAttachImage), - AttachmentAction("Live camera", "Look around with vision", RACIcons.Outline.DeviceMobile, onOpenLive), - AttachmentAction("Advanced tools", "SDK demos and diagnostics", RACIcons.Outline.Stack, onOpenAdvanced), + AttachmentAction( + "Live camera", + "Look around with vision", + RACIcons.Outline.DeviceMobile, + onOpenLive + ), + AttachmentAction( + "Advanced tools", + "SDK demos and diagnostics", + RACIcons.Outline.Stack, + onOpenAdvanced + ), ) Column( @@ -134,8 +154,8 @@ fun ChatInputBar( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = dimens.spacingMd, vertical = dimens.spacingSm), - verticalAlignment = Alignment.Bottom, + .padding(horizontal = dimens.spacingMd).padding(top = dimens.spacingSm), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(dimens.spacingSm), ) { Box { @@ -148,7 +168,7 @@ fun ChatInputBar( ), ) { Icon( - imageVector = RACIcons.Outline.Plus, + imageVector = RACIcons.Outline.Menu, contentDescription = "Attach or open a mode", modifier = Modifier.size(dimens.iconMd), ) @@ -181,6 +201,8 @@ fun ChatInputBar( } } + Spacer(modifier = Modifier.weight(1f)) + IconButton( onClick = onToggleTools, modifier = Modifier.size(dimens.inputBarMinHeight), @@ -208,6 +230,59 @@ fun ChatInputBar( ) } + IconButton( + onClick = onOpenTalk, + modifier = Modifier.size(dimens.inputBarMinHeight), + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Icon( + imageVector = RACIcons.Outline.Microphone, + contentDescription = "Talk mode", + modifier = Modifier.size(dimens.iconMd), + ) + } + + IconButton( + onClick = onToggleThinking, + enabled = thinkingSupported, + modifier = Modifier.size(dimens.inputBarMinHeight), + colors = IconButtonDefaults.iconButtonColors( + containerColor = if (thinkingEnabled) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.15f) + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + contentColor = if (thinkingEnabled) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + ), + ) { + Icon( + imageVector = RACIcons.Outline.Brain, + contentDescription = when { + !thinkingSupported -> "Thinking not supported by current model" + thinkingEnabled -> "Disable thinking" + else -> "Enable thinking" + }, + modifier = Modifier.size(dimens.iconMd), + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimens.spacingMd, vertical = dimens.spacingSm), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(dimens.spacingSm), + ) { Box( modifier = Modifier .weight(1f) @@ -242,21 +317,6 @@ fun ChatInputBar( ) } - IconButton( - onClick = onOpenTalk, - modifier = Modifier.size(dimens.inputBarMinHeight), - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) { - Icon( - imageVector = RACIcons.Outline.Microphone, - contentDescription = "Talk mode", - modifier = Modifier.size(dimens.iconMd), - ) - } - val haptics = androidx.compose.ui.platform.LocalHapticFeedback.current IconButton( onClick = { @@ -337,7 +397,11 @@ private fun AttachmentStatusPill( horizontalArrangement = Arrangement.spacedBy(dimens.spacingSm), verticalAlignment = Alignment.CenterVertically, ) { - Icon(attachment.icon, contentDescription = null, modifier = Modifier.size(dimens.iconSm)) + Icon( + attachment.icon, + contentDescription = null, + modifier = Modifier.size(dimens.iconSm) + ) Column(modifier = Modifier.weight(1f, fill = false)) { Text( attachment.name, @@ -355,7 +419,11 @@ private fun AttachmentStatusPill( ) } IconButton(onClick = onClear, modifier = Modifier.size(32.dp)) { - Icon(RACIcons.Outline.Close, contentDescription = "Remove attachment", modifier = Modifier.size(dimens.iconSm)) + Icon( + RACIcons.Outline.Close, + contentDescription = "Remove attachment", + modifier = Modifier.size(dimens.iconSm) + ) } } } diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatScreen.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatScreen.kt index 43cffa2cb3..5181f0e953 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatScreen.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatScreen.kt @@ -238,6 +238,9 @@ fun ChatScreen( onOpenLive = onOpenVision, onOpenTalk = onOpenVoice, onOpenAdvanced = onOpenAdvanced, + onToggleThinking = viewModel::toggleThinking, + thinkingEnabled = viewModel.thinkingEnabled, + thinkingSupported = viewModel.thinkingSupported, modifier = Modifier.widthIn(max = dimens.contentMaxWidth), pendingAttachment = pendingAttachment?.toComposerAttachment(), onClearAttachment = { pendingAttachment = null }, diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt index cb6e80aeeb..954583a9d9 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt @@ -132,6 +132,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } + val thinkingSupported: Boolean + get() = GlobalState.model.loaded?.supports_thinking == true + + val thinkingEnabled: Boolean + get() = thinkingSupported && !SettingsRepository.settings.disableThinking + + fun toggleThinking() { + SettingsRepository.setDisableThinking(!SettingsRepository.settings.disableThinking) + } + val canSend: Boolean get() = input.isNotBlank() && !isBusy && !generationOwnership.isBusy() && GlobalState.model.isLoaded From 34e146ce8c55f1899c8029a0a42bfad39048f9bd Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 11:43:41 +0530 Subject: [PATCH 08/44] Add telemetry extraction unit tests (per-modality routing + field regression guards) --- sdk/runanywhere-commons/tests/CMakeLists.txt | 13 + .../tests/test_telemetry_extraction.cpp | 233 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp diff --git a/sdk/runanywhere-commons/tests/CMakeLists.txt b/sdk/runanywhere-commons/tests/CMakeLists.txt index 7a8b6ffd47..9a18183b9c 100644 --- a/sdk/runanywhere-commons/tests/CMakeLists.txt +++ b/sdk/runanywhere-commons/tests/CMakeLists.txt @@ -568,6 +568,19 @@ target_compile_features(test_sdk_events_service_proto_abi PRIVATE cxx_std_17) rac_test_define_have_protobuf(test_sdk_events_service_proto_abi) add_test(NAME sdk_events_service_proto_abi_tests COMMAND test_sdk_events_service_proto_abi) +# Telemetry extraction → routing → JSON pipeline (regression guards for the +# per-modality telemetry field bugs). Needs protobuf for the SDKEvent builders. +add_executable(test_telemetry_extraction test_telemetry_extraction.cpp) +target_include_directories(test_telemetry_extraction PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/include +) +target_link_libraries(test_telemetry_extraction PRIVATE rac_commons) +rac_link_archive_deps(test_telemetry_extraction) +target_compile_features(test_telemetry_extraction PRIVATE cxx_std_17) +rac_test_define_have_protobuf(test_telemetry_extraction) +add_test(NAME telemetry_extraction_tests COMMAND test_telemetry_extraction) + # --- ToolCalling generated service descriptor test -------------------------- add_executable(test_tool_calling_service_proto_abi test_tool_calling_service_proto_abi.cpp) target_include_directories(test_tool_calling_service_proto_abi PRIVATE diff --git a/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp b/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp new file mode 100644 index 0000000000..754136e027 --- /dev/null +++ b/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp @@ -0,0 +1,233 @@ +/** + * @file test_telemetry_extraction.cpp + * @brief Unit tests for the telemetry extraction → routing → JSON pipeline. + * + * Feeds canonical runanywhere.v1.SDKEvent protos through + * rac_telemetry_manager_track_proto() with a capturing HTTP callback (no + * network, no models) and asserts the outgoing endpoint + JSON body per + * modality. These are regression guards for the telemetry field bugs fixed in + * commons: + * - LLM token undercount / tokens_per_second (decode-loop count) + * - LLM/VLM prompt_eval_time_ms on the result + * - STT NaN confidence producing invalid JSON ("confidence":nan) + * - embeddings total_tokens / batch_size + * - LoRA failure-path base_model_id / adapter_id / adapter_size_bytes + * - RAG query_token_count / context_tokens + * + * DEVELOPMENT env is used so the flush auth-gate (rac_env_requires_auth) is + * bypassed and completion events flush synchronously into the capture callback. + */ + +#include +#include +#include + +#include "rac/infrastructure/network/rac_environment.h" +#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" + +#if defined(RAC_HAVE_PROTOBUF) +#include "sdk_events.pb.h" +namespace v1 = runanywhere::v1; +#endif + +static int g_checks = 0; +static int g_failures = 0; + +#define CHECK(cond, msg) \ + do { \ + ++g_checks; \ + if (!(cond)) { \ + ++g_failures; \ + std::fprintf(stderr, " FAIL: %s\n", (msg)); \ + } \ + } while (0) + +#if defined(RAC_HAVE_PROTOBUF) + +namespace { + +struct Capture { + bool called = false; + std::string endpoint; + std::string body; +}; + +void capture_cb(void* user_data, const char* endpoint, const char* json_body, size_t json_length, + rac_bool_t /*requires_auth*/) { + auto* c = static_cast(user_data); + c->called = true; + c->endpoint = endpoint != nullptr ? endpoint : ""; + c->body.assign(json_body != nullptr ? json_body : "", json_body != nullptr ? json_length : 0); +} + +bool has(const std::string& hay, const std::string& needle) { + return hay.find(needle) != std::string::npos; +} + +void envelope(v1::SDKEvent* ev, v1::SDKComponent component) { + ev->set_id("test-event"); + ev->set_timestamp_ms(1); + ev->set_component(component); + ev->set_source("cpp"); +} + +// Serialize + track one event; the completion flush fires the capture callback +// inline. Marks the in-flight batch complete afterward so state stays clean. +void track(rac_telemetry_manager_t* mgr, Capture* cap, const v1::SDKEvent& ev) { + cap->called = false; + cap->endpoint.clear(); + cap->body.clear(); + const std::string bytes = ev.SerializeAsString(); + rac_telemetry_manager_track_proto(mgr, reinterpret_cast(bytes.data()), + bytes.size()); + rac_telemetry_manager_http_complete(mgr, RAC_TRUE, nullptr, nullptr); +} + +} // namespace + +#endif // RAC_HAVE_PROTOBUF + +int main() { + std::fprintf(stdout, "test_telemetry_extraction\n"); + +#if !defined(RAC_HAVE_PROTOBUF) + std::fprintf(stdout, " skip: telemetry extraction tests (no protobuf)\n"); + return 0; +#else + rac_telemetry_manager_t* mgr = + rac_telemetry_manager_create(RAC_ENV_DEVELOPMENT, "test-device", "linux", "0.20.11"); + CHECK(mgr != nullptr, "telemetry manager created"); + if (mgr == nullptr) { + return 1; + } + Capture cap; + rac_telemetry_manager_set_http_callback(mgr, capture_cb, &cap); + + // --- LLM: token count + tokens_per_second + prompt_eval_time_ms ---------- + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_LLM); + auto* g = ev.mutable_generation(); + g->set_kind(v1::GENERATION_EVENT_KIND_COMPLETED); + g->set_model_id("qwen3-0.6b"); + g->set_input_tokens(61); + g->set_tokens_used(256); + g->set_tokens_per_second(44.4); + g->set_prompt_eval_time_ms(437); + track(mgr, &cap, ev); + CHECK(cap.called, "llm: event delivered to sink"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/llm", "llm: routed to llm endpoint"); + CHECK(has(cap.body, "\"output_tokens\":256"), "llm: output_tokens = 256"); + CHECK(has(cap.body, "\"prompt_eval_time_ms\":437"), "llm: prompt_eval_time_ms = 437"); + CHECK(has(cap.body, "tokens_per_second"), "llm: tokens_per_second present"); + } + + // --- STT with NaN confidence: JSON must stay valid (no "nan") ----------- + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_STT); + auto* vo = ev.mutable_voice(); // VoiceLifecycleEvent + vo->set_kind(v1::VOICE_EVENT_KIND_STT_COMPLETED); + vo->set_model_id("whisper-tiny.en"); + vo->set_confidence(std::nanf("")); // whisper-tiny emits NaN confidence + vo->set_real_time_factor(0.5); + vo->set_word_count(4); + vo->set_audio_length_ms(2000); + track(mgr, &cap, ev); + CHECK(cap.called, "stt: event delivered to sink"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/stt", "stt: routed to stt endpoint"); + CHECK(!has(cap.body, "nan") && !has(cap.body, "NaN"), + "stt: NaN confidence does not leak into JSON"); + CHECK(has(cap.body, "real_time_factor") || has(cap.body, "word_count"), + "stt: fields present"); + } + + // --- Embeddings: total_tokens + batch_size + embedding_dimension -------- + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_EMBEDDINGS); + (*ev.mutable_properties())["embedding_dimension"] = "384"; + (*ev.mutable_properties())["total_tokens"] = "21"; + (*ev.mutable_properties())["batch_size"] = "1"; + auto* cap_ev = ev.mutable_capability(); + cap_ev->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_EMBEDDINGS_COMPLETED); + cap_ev->set_component(v1::SDK_COMPONENT_EMBEDDINGS); + cap_ev->set_model_id("all-minilm-l6-v2"); + cap_ev->set_input_count(1); + cap_ev->set_output_count(1); + track(mgr, &cap, ev); + CHECK(cap.called, "embeddings: event delivered to sink"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/embeddings", "embeddings: routed correctly"); + CHECK(has(cap.body, "\"total_tokens\":21"), "embeddings: total_tokens = 21"); + CHECK(has(cap.body, "\"batch_size\":1"), "embeddings: batch_size = 1"); + CHECK(has(cap.body, "\"embedding_dimension\":384"), "embeddings: embedding_dimension = 384"); + } + + // --- LoRA failure: base_model_id + adapter_id + adapter_size_bytes ------ + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_LLM); // LoRA rides on the LLM component + (*ev.mutable_properties())["adapter_id"] = "my-test-adapter"; + (*ev.mutable_properties())["adapter_size_bytes"] = "4096"; + auto* cap_ev = ev.mutable_capability(); + cap_ev->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_LORA_FAILED); + cap_ev->set_component(v1::SDK_COMPONENT_LLM); + cap_ev->set_model_id("smollm2-360m-q8_0"); // base model + track(mgr, &cap, ev); + CHECK(cap.called, "lora: event delivered to sink"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/lora", "lora: routed to lora endpoint"); + CHECK(has(cap.body, "\"operation\":\"failed\""), "lora: operation = failed"); + CHECK(has(cap.body, "smollm2-360m-q8_0"), "lora: base_model_id present"); + CHECK(has(cap.body, "\"adapter_id\":\"my-test-adapter\""), "lora: adapter_id present"); + CHECK(has(cap.body, "\"adapter_size_bytes\":4096"), "lora: adapter_size_bytes = 4096"); + } + + // --- RAG query: retrieved_docs_count + top_k + query/context tokens ----- + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_RAG); + (*ev.mutable_properties())["top_k"] = "5"; + (*ev.mutable_properties())["retrieval_time_ms"] = "1"; + (*ev.mutable_properties())["embedding_model"] = "all-minilm-l6-v2"; + (*ev.mutable_properties())["query_token_count"] = "10"; + (*ev.mutable_properties())["context_tokens"] = "49"; + auto* cap_ev = ev.mutable_capability(); + cap_ev->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_RAG_QUERY_COMPLETED); + cap_ev->set_component(v1::SDK_COMPONENT_RAG); + cap_ev->set_model_id("smollm2-360m-q8_0"); + cap_ev->set_output_count(2); // retrieved docs + track(mgr, &cap, ev); + CHECK(cap.called, "rag: event delivered to sink"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/rag", "rag: routed to rag endpoint"); + CHECK(has(cap.body, "\"retrieved_docs_count\":2"), "rag: retrieved_docs_count = 2"); + CHECK(has(cap.body, "\"top_k\":5"), "rag: top_k = 5"); + CHECK(has(cap.body, "\"query_token_count\":10"), "rag: query_token_count = 10"); + CHECK(has(cap.body, "\"context_tokens\":49"), "rag: context_tokens = 49"); + } + + // --- VLM: image_count + prompt_eval_time_ms ----------------------------- + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_VLM); + (*ev.mutable_properties())["total_tokens"] = "124"; + (*ev.mutable_properties())["tokens_per_second"] = "116.7"; + (*ev.mutable_properties())["prompt_eval_time_ms"] = "826"; + auto* cap_ev = ev.mutable_capability(); + cap_ev->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_VLM_COMPLETED); + cap_ev->set_component(v1::SDK_COMPONENT_VLM); + cap_ev->set_model_id("smolvlm2-256m"); + cap_ev->set_input_count(1); // image count + cap_ev->set_output_count(128); + track(mgr, &cap, ev); + CHECK(cap.called, "vlm: event delivered to sink"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/vlm", "vlm: routed to vlm endpoint"); + CHECK(has(cap.body, "\"image_count\":1"), "vlm: image_count = 1"); + CHECK(has(cap.body, "\"prompt_eval_time_ms\":826"), "vlm: prompt_eval_time_ms = 826"); + } + + rac_telemetry_manager_destroy(mgr); + + std::fprintf(stdout, " %d checks, %d failures\n", g_checks, g_failures); + return g_failures == 0 ? 0 : 1; +#endif // RAC_HAVE_PROTOBUF +} From 125203e88cf14401bf9df4594cfc91873d6d8867 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 11:55:19 +0530 Subject: [PATCH 09/44] Add live telemetry integration test (--live): authenticated per-modality POST asserting backend 2xx --- sdk/runanywhere-cli/src/bootstrap.cpp | 2 + sdk/runanywhere-cli/src/bootstrap.h | 9 + sdk/runanywhere-cli/tests/CMakeLists.txt | 10 + .../tests/test_rcli_telemetry_live.cpp | 294 ++++++++++++++++++ 4 files changed, 315 insertions(+) create mode 100644 sdk/runanywhere-cli/tests/test_rcli_telemetry_live.cpp diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index 40c478cb69..6ddaa1fef9 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -552,4 +552,6 @@ void shutdown() { } } +rac_telemetry_manager_t *active_telemetry_manager() { return g_telemetry_manager; } + } // namespace rcli diff --git a/sdk/runanywhere-cli/src/bootstrap.h b/sdk/runanywhere-cli/src/bootstrap.h index 7842cfa03d..eb24e012aa 100644 --- a/sdk/runanywhere-cli/src/bootstrap.h +++ b/sdk/runanywhere-cli/src/bootstrap.h @@ -18,6 +18,8 @@ #include "rac/core/rac_types.h" +typedef struct rac_telemetry_manager rac_telemetry_manager_t; + namespace rcli { /** Global flags shared by all subcommands (parsed in main.cpp). */ @@ -46,6 +48,13 @@ rac_result_t bootstrap(const GlobalOptions& options, Bootstrapped* out); /** rac_shutdown() wrapper; safe to call when bootstrap never ran. */ void shutdown(); +/** + * The process telemetry manager created by bootstrap() (NULL if telemetry was + * not initialized, e.g. no creds). Exposed for the live telemetry integration + * test, which overrides its HTTP callback to observe the backend's response. + */ +rac_telemetry_manager_t* active_telemetry_manager(); + } // namespace rcli #endif // RCLI_BOOTSTRAP_H diff --git a/sdk/runanywhere-cli/tests/CMakeLists.txt b/sdk/runanywhere-cli/tests/CMakeLists.txt index 7b6d257297..a67d13d27a 100644 --- a/sdk/runanywhere-cli/tests/CMakeLists.txt +++ b/sdk/runanywhere-cli/tests/CMakeLists.txt @@ -6,6 +6,16 @@ target_include_directories(test_rcli_unit PRIVATE target_link_libraries(test_rcli_unit PRIVATE rcli_core) add_test(NAME rcli_unit_tests COMMAND test_rcli_unit --run-all) +# Live telemetry integration — real authenticated POST per modality against the +# configured backend. Opt-in: no-ops without `--live` + creds, so it stays safe +# in ctest/CI. Links rcli_core for bootstrap() + the HTTP transport + auth. +add_executable(test_rcli_telemetry_live test_rcli_telemetry_live.cpp) +target_include_directories(test_rcli_telemetry_live PRIVATE + ${CMAKE_SOURCE_DIR}/sdk/runanywhere-commons/tests +) +target_link_libraries(test_rcli_telemetry_live PRIVATE rcli_core) +add_test(NAME rcli_telemetry_live_tests COMMAND test_rcli_telemetry_live) + if(TARGET rac_backend_mlx) add_executable(test_rcli_mlx_e2e test_rcli_mlx_e2e.cpp) target_include_directories(test_rcli_mlx_e2e PRIVATE diff --git a/sdk/runanywhere-cli/tests/test_rcli_telemetry_live.cpp b/sdk/runanywhere-cli/tests/test_rcli_telemetry_live.cpp new file mode 100644 index 0000000000..0cbbd791fe --- /dev/null +++ b/sdk/runanywhere-cli/tests/test_rcli_telemetry_live.cpp @@ -0,0 +1,294 @@ +/** + * @file test_rcli_telemetry_live.cpp + * @brief Live telemetry integration test — sends real, authenticated per-modality + * telemetry to the configured backend and asserts each is accepted (2xx). + * + * This complements the hermetic commons unit test (test_telemetry_extraction), + * which validates JSON shape offline against a mock sink. Here we exercise the + * full wire path: rcli bootstrap() registers the desktop adapter + HTTP + * transport and authenticates (API key -> device register -> JWT); we then + * override the process telemetry manager's HTTP callback with a status-recording + * POST (the same recipe as bootstrap's rcli_telemetry_http_callback) so we can + * assert the backend's response. A strict-schema rejection (422 extra_forbidden) + * fails the test — catching field drift against the real V2 endpoints. + * + * Opt-in: runs ONLY when invoked with `--live` AND the creds are in the + * environment (RUNANYWHERE_BASE_URL + RUNANYWHERE_API_KEY, optional + * RUNANYWHERE_ENVIRONMENT). Without those it prints a skip and exits 0, so it is + * safe to leave registered in ctest / CI (which run it with no args). + * + * RUNANYWHERE_BASE_URL=... RUNANYWHERE_API_KEY=... \ + * ./test_rcli_telemetry_live --live + */ + +#include +#include +#include +#include +#include + +#include "bootstrap.h" + +#include "rac/core/rac_sdk_state.h" +#include "rac/infrastructure/http/rac_http_client.h" +#include "rac/infrastructure/http/rac_http_transport.h" +#include "rac/infrastructure/network/rac_auth_manager.h" +#include "rac/infrastructure/network/rac_endpoints.h" +#include "rac/infrastructure/network/rac_environment.h" +#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" + +#if defined(RAC_HAVE_PROTOBUF) +#include "sdk_events.pb.h" +namespace v1 = runanywhere::v1; +#endif + +static int g_checks = 0; +static int g_failures = 0; + +#define CHECK(cond, msg) \ + do { \ + ++g_checks; \ + if (!(cond)) { \ + ++g_failures; \ + std::fprintf(stderr, " FAIL: %s\n", (msg)); \ + } \ + } while (0) + +#if defined(RAC_HAVE_PROTOBUF) + +namespace { + +// Records the backend's response for the most recent flushed batch, then hands +// the result back to the manager. POST recipe mirrors bootstrap's +// rcli_telemetry_http_callback (all-public rac_http_* / rac_auth_* APIs). +struct LiveState { + rac_telemetry_manager_t* manager = nullptr; + bool called = false; + int status = 0; + bool ok = false; + std::string endpoint; + std::string body; +}; + +void live_post_cb(void* user_data, const char* endpoint, const char* json_body, size_t json_length, + rac_bool_t requires_auth) { + auto* st = static_cast(user_data); + st->called = true; + st->endpoint = endpoint != nullptr ? endpoint : ""; + st->status = 0; + st->ok = false; + st->body.clear(); + + const char* base_url = rac_state_get_base_url(); + if (base_url == nullptr || base_url[0] == '\0' || + rac_http_transport_is_registered() != RAC_TRUE) { + rac_telemetry_manager_http_complete(st->manager, RAC_FALSE, nullptr, "transport unavailable"); + return; + } + char url[2048] = {}; + if (rac_build_url(base_url, endpoint, url, sizeof(url)) < 0) { + rac_telemetry_manager_http_complete(st->manager, RAC_FALSE, nullptr, "url build failed"); + return; + } + + std::vector headers; + const rac_http_header_kv_t* defaults = nullptr; + size_t default_count = 0; + if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && defaults != nullptr) { + headers.assign(defaults, defaults + default_count); + } + std::string auth_value; + if (requires_auth == RAC_TRUE) { + const char* token = rac_auth_get_access_token(); + if (token != nullptr && token[0] != '\0') { + auth_value = std::string("Bearer ") + token; + headers.push_back({"Authorization", auth_value.c_str()}); + } + } + + rac_http_client_t* client = nullptr; + if (rac_http_client_create(&client) != RAC_SUCCESS) { + rac_telemetry_manager_http_complete(st->manager, RAC_FALSE, nullptr, "client create failed"); + return; + } + rac_http_request_t request = {}; + request.method = "POST"; + request.url = url; + request.headers = headers.empty() ? nullptr : headers.data(); + request.header_count = headers.size(); + request.body_bytes = reinterpret_cast(json_body); + request.body_len = json_length; + request.timeout_ms = rac_env_default_http_timeout_ms(rac_state_get_environment()); + request.follow_redirects = RAC_FALSE; + + rac_http_response_t response = {}; + const rac_result_t rc = rac_http_request_send(client, &request, &response); + rac_http_client_destroy(client); + + st->status = response.status; + st->ok = rc == RAC_SUCCESS && response.status >= 200 && response.status < 300; + if (response.body_bytes != nullptr && response.body_len > 0) { + st->body.assign(reinterpret_cast(response.body_bytes), response.body_len); + } + rac_telemetry_manager_http_complete(st->manager, st->ok ? RAC_TRUE : RAC_FALSE, + st->body.empty() ? nullptr : st->body.c_str(), + st->ok ? nullptr : "POST failed"); + rac_http_response_free(&response); +} + +void envelope(v1::SDKEvent* ev, v1::SDKComponent component) { + ev->set_id("rcli-live-test"); + ev->set_timestamp_ms(1); + ev->set_component(component); + ev->set_source("cpp"); +} + +// Send one event and assert the backend accepted it (2xx). +void send_and_assert(rac_telemetry_manager_t* mgr, LiveState* st, const v1::SDKEvent& ev, + const char* label) { + st->called = false; + const std::string bytes = ev.SerializeAsString(); + rac_telemetry_manager_track_proto(mgr, reinterpret_cast(bytes.data()), + bytes.size()); + if (!st->called) { + // No completion flush fired (unexpected for a completion event). + ++g_checks; + ++g_failures; + std::fprintf(stderr, " FAIL: %s: no POST was made\n", label); + return; + } + if (!st->ok) { + std::fprintf(stderr, " %s: http=%d body=%s\n", label, st->status, + st->body.empty() ? "(empty)" : st->body.c_str()); + } else { + std::fprintf(stdout, " %s: accepted (http=%d, %s)\n", label, st->status, + st->endpoint.c_str()); + } + CHECK(st->ok, label); +} + +} // namespace + +#endif // RAC_HAVE_PROTOBUF + +int main(int argc, char** argv) { + std::fprintf(stdout, "test_rcli_telemetry_live\n"); + + bool live = false; + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--live") == 0) { + live = true; + } + } + const char* base = std::getenv("RUNANYWHERE_BASE_URL"); + const char* key = std::getenv("RUNANYWHERE_API_KEY"); + const bool have_creds = base != nullptr && base[0] != '\0' && key != nullptr && key[0] != '\0'; + + if (!live || !have_creds) { + std::fprintf(stdout, + " skip: live telemetry test (needs --live and " + "RUNANYWHERE_BASE_URL + RUNANYWHERE_API_KEY)\n"); + return 0; + } + +#if !defined(RAC_HAVE_PROTOBUF) + std::fprintf(stdout, " skip: no protobuf\n"); + return 0; +#else + rcli::GlobalOptions opts; + opts.quiet = true; + rcli::Bootstrapped env; + const rac_result_t brc = rcli::bootstrap(opts, &env); + CHECK(brc == RAC_SUCCESS, "bootstrap succeeded"); + if (brc != RAC_SUCCESS) { + return 1; + } + + rac_telemetry_manager_t* mgr = rcli::active_telemetry_manager(); + CHECK(mgr != nullptr, "telemetry manager initialized (creds + auth)"); + if (mgr == nullptr) { + rcli::shutdown(); + return 1; + } + + LiveState state; + state.manager = mgr; + rac_telemetry_manager_set_http_callback(mgr, live_post_cb, &state); + + // LLM + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_LLM); + auto* g = ev.mutable_generation(); + g->set_kind(v1::GENERATION_EVENT_KIND_COMPLETED); + g->set_model_id("rcli-live-test"); + g->set_input_tokens(10); + g->set_tokens_used(20); + g->set_tokens_per_second(40.0); + g->set_prompt_eval_time_ms(100); + send_and_assert(mgr, &state, ev, "llm"); + } + // Embeddings + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_EMBEDDINGS); + (*ev.mutable_properties())["embedding_dimension"] = "384"; + (*ev.mutable_properties())["total_tokens"] = "8"; + (*ev.mutable_properties())["batch_size"] = "1"; + auto* c = ev.mutable_capability(); + c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_EMBEDDINGS_COMPLETED); + c->set_component(v1::SDK_COMPONENT_EMBEDDINGS); + c->set_model_id("rcli-live-test"); + c->set_input_count(1); + c->set_output_count(1); + send_and_assert(mgr, &state, ev, "embeddings"); + } + // RAG + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_RAG); + (*ev.mutable_properties())["top_k"] = "5"; + (*ev.mutable_properties())["retrieval_time_ms"] = "1"; + (*ev.mutable_properties())["embedding_model"] = "rcli-live-test"; + (*ev.mutable_properties())["query_token_count"] = "10"; + (*ev.mutable_properties())["context_tokens"] = "49"; + auto* c = ev.mutable_capability(); + c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_RAG_QUERY_COMPLETED); + c->set_component(v1::SDK_COMPONENT_RAG); + c->set_model_id("rcli-live-test"); + c->set_output_count(2); + send_and_assert(mgr, &state, ev, "rag"); + } + // VLM + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_VLM); + (*ev.mutable_properties())["total_tokens"] = "120"; + (*ev.mutable_properties())["tokens_per_second"] = "100.0"; + (*ev.mutable_properties())["prompt_eval_time_ms"] = "800"; + auto* c = ev.mutable_capability(); + c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_VLM_COMPLETED); + c->set_component(v1::SDK_COMPONENT_VLM); + c->set_model_id("rcli-live-test"); + c->set_input_count(1); + c->set_output_count(120); + send_and_assert(mgr, &state, ev, "vlm"); + } + // LoRA (failure path — rides the LLM component, modality overridden to lora) + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_LLM); + (*ev.mutable_properties())["adapter_id"] = "rcli-live-test"; + (*ev.mutable_properties())["adapter_size_bytes"] = "4096"; + auto* c = ev.mutable_capability(); + c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_LORA_FAILED); + c->set_component(v1::SDK_COMPONENT_LLM); + c->set_model_id("rcli-live-test"); + send_and_assert(mgr, &state, ev, "lora"); + } + + rcli::shutdown(); + std::fprintf(stdout, " %d checks, %d failures\n", g_checks, g_failures); + return g_failures == 0 ? 0 : 1; +#endif // RAC_HAVE_PROTOBUF +} From b6a07145fd9887a5646623b9fc1d877e80f4487a Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 13:36:35 +0530 Subject: [PATCH 10/44] Allowlist Kotlin web-search tool JSON surface (external DuckDuckGo response, mirrors Swift) --- scripts/validation/gates/deprecated_surface_allowlist.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/validation/gates/deprecated_surface_allowlist.txt b/scripts/validation/gates/deprecated_surface_allowlist.txt index d78e165f73..96b53a9efe 100644 --- a/scripts/validation/gates/deprecated_surface_allowlist.txt +++ b/scripts/validation/gates/deprecated_surface_allowlist.txt @@ -9,6 +9,9 @@ sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/CloudSttProvider.kt|kotlin:org-json-usage sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/Cloud.kt|kotlin:org-json-usage sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/StructuredOutputProtoHelpers.kt|kotlin:json-serialisation +# Web-search tool parses the external DuckDuckGo JSON response (not an SDK DTO); +# mirrors the already-allowlisted Swift RunAnywhere+WebSearchTool.swift. +sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt|kotlin:json-serialisation # Swift public value types and typed JSON boundaries. sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/ToolCallingTypes.swift|swift:types-dto-file From 4383a4b5424108e465affd398d1cfd11ae9dbdac Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 14:03:49 +0530 Subject: [PATCH 11/44] Revert "Fix LLM streaming token undercount: thread authoritative decode count through vtable generate_stream" This reverts commit 607cb638734820db1bbda1c685ca0a9957839231. --- .../llamacpp/rac_backend_llamacpp_register.cpp | 5 ++--- engines/llamacpp/rac_llm_llamacpp.cpp | 15 +++------------ engines/mlx/rac_mlx_engine.cpp | 7 +------ .../include/rac/backends/rac_llm_llamacpp.h | 3 +-- .../include/rac/features/llm/rac_llm_service.h | 12 ++---------- .../src/features/llm/llm_module.cpp | 16 +++++----------- .../src/features/llm/rac_llm_service.cpp | 2 +- .../src/features/llm/structured_output.cpp | 3 +-- 8 files changed, 16 insertions(+), 47 deletions(-) diff --git a/engines/llamacpp/rac_backend_llamacpp_register.cpp b/engines/llamacpp/rac_backend_llamacpp_register.cpp index d3f190cc3d..76f302b5e4 100644 --- a/engines/llamacpp/rac_backend_llamacpp_register.cpp +++ b/engines/llamacpp/rac_backend_llamacpp_register.cpp @@ -176,11 +176,10 @@ static rac_bool_t stream_adapter_callback(const char* token, rac_bool_t is_final static rac_result_t llamacpp_vtable_generate_stream(void* impl, const char* prompt, const rac_llm_options_t* options, rac_llm_stream_callback_fn callback, - void* user_data, - int32_t* out_tokens_generated) { + void* user_data) { StreamAdapter adapter = {callback, user_data}; return rac_llm_llamacpp_generate_stream(legacy_handle(impl), prompt, options, - stream_adapter_callback, &adapter, out_tokens_generated); + stream_adapter_callback, &adapter); } // Get info diff --git a/engines/llamacpp/rac_llm_llamacpp.cpp b/engines/llamacpp/rac_llm_llamacpp.cpp index db3a2e0ccb..ce570e7081 100644 --- a/engines/llamacpp/rac_llm_llamacpp.cpp +++ b/engines/llamacpp/rac_llm_llamacpp.cpp @@ -353,10 +353,7 @@ rac_result_t rac_llm_llamacpp_generate(rac_handle_t handle, const char* prompt, rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* prompt, const rac_llm_options_t* options, rac_llm_llamacpp_stream_callback_fn callback, - void* user_data, int32_t* out_tokens_generated) { - if (out_tokens_generated != nullptr) { - *out_tokens_generated = 0; - } + void* user_data) { if (handle == nullptr || prompt == nullptr || callback == nullptr) { return RAC_ERROR_NULL_POINTER; } @@ -424,11 +421,9 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p bool terminal_emitted = false; bool success = false; - int decoded_tokens = 0; try { success = h->text_gen->generate_stream( - request, - [&](const std::string& token) -> bool { + request, [&](const std::string& token) -> bool { if (user_stops.empty()) { return callback(token.c_str(), RAC_FALSE, user_data) == RAC_TRUE; } @@ -450,8 +445,7 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p return callback(safe_chunk.c_str(), RAC_FALSE, user_data) == RAC_TRUE; } return true; - }, - /*out_prompt_tokens=*/nullptr, /*out_prompt_eval_ms=*/nullptr, &decoded_tokens); + }); } catch (const std::exception& e) { rac_error_set_details(e.what()); if (!terminal_emitted) { @@ -480,9 +474,6 @@ rac_result_t rac_llm_llamacpp_generate_stream(rac_handle_t handle, const char* p } callback("", RAC_TRUE, user_data); // Final token terminal_emitted = true; - if (out_tokens_generated != nullptr) { - *out_tokens_generated = decoded_tokens; - } return RAC_SUCCESS; } diff --git a/engines/mlx/rac_mlx_engine.cpp b/engines/mlx/rac_mlx_engine.cpp index 5fd63c9da1..c831b7cf5a 100644 --- a/engines/mlx/rac_mlx_engine.cpp +++ b/engines/mlx/rac_mlx_engine.cpp @@ -328,12 +328,7 @@ rac_result_t llm_generate(void* impl, const char* prompt, const rac_llm_options_ } rac_result_t llm_generate_stream(void* impl, const char* prompt, const rac_llm_options_t* options, - rac_llm_stream_callback_fn callback, void* user_data, - int32_t* out_tokens_generated) { - // The MLX Swift callback path does not surface a decoded-token count here; - // leave out_tokens_generated untouched so callers fall back to the streaming - // callback count. - (void)out_tokens_generated; + rac_llm_stream_callback_fn callback, void* user_data) { if (!prompt || !callback) { return RAC_ERROR_NULL_POINTER; } diff --git a/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h b/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h index 1dc106088a..f6b352ff27 100644 --- a/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h +++ b/sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h @@ -148,8 +148,7 @@ typedef rac_bool_t (*rac_llm_llamacpp_stream_callback_fn)(const char* token, rac */ RAC_LLAMACPP_API rac_result_t rac_llm_llamacpp_generate_stream( rac_handle_t handle, const char* prompt, const rac_llm_options_t* options, - rac_llm_llamacpp_stream_callback_fn callback, void* user_data, - int32_t* out_tokens_generated); + rac_llm_llamacpp_stream_callback_fn callback, void* user_data); /** * Cancels ongoing generation. diff --git a/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h b/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h index e422d4473e..ed718edb74 100644 --- a/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h +++ b/sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h @@ -50,18 +50,10 @@ typedef struct rac_llm_service_ops { rac_result_t (*generate)(void* impl, const char* prompt, const rac_llm_options_t* options, rac_llm_result_t* out_result); - /** Generate text with streaming callback. - * - * out_tokens_generated (optional, may be NULL): receives the backend's - * authoritative decoded-token count. Prefer this over counting streaming - * callback invocations — the callback delivers buffered chunks, not one call - * per token, so callback counts under-report generated tokens. A backend - * that does not populate it leaves it untouched; callers fall back to the - * callback count. */ + /** Generate text with streaming callback */ rac_result_t (*generate_stream)(void* impl, const char* prompt, const rac_llm_options_t* options, - rac_llm_stream_callback_fn callback, void* user_data, - int32_t* out_tokens_generated); + rac_llm_stream_callback_fn callback, void* user_data); /** Get service info */ rac_result_t (*get_info)(void* impl, rac_llm_info_t* out_info); diff --git a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp index 7950826398..cd937de675 100644 --- a/sdk/runanywhere-commons/src/features/llm/llm_module.cpp +++ b/sdk/runanywhere-commons/src/features/llm/llm_module.cpp @@ -2253,10 +2253,9 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, // SDKs it would be undefined behaviour through a C ABI return. const std::string effective_prompt = rac::llm::apply_no_think_directive(request.prompt(), options.disable_thinking); - int32_t backend_tokens_generated = 0; try { rc = ref.ops->generate_stream(ref.impl, effective_prompt.c_str(), &options, - stream_token_callback, &ctx, &backend_tokens_generated); + stream_token_callback, &ctx); } catch (const std::exception& e) { rac_error_set_details(e.what()); rc = RAC_ERROR_INFERENCE_FAILED; @@ -2289,13 +2288,8 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, // streaming proto generation looks like a natural stop, which breaks // OpenAI parity for direct streaming proto callers (JNI, Web, etc.) // and diverges from the non-streaming proto path. - // Prefer the backend's authoritative decoded-token count; the streaming - // callback fires per buffered chunk, so ctx.token_count (callback count) - // under-reports. Fall back to it only when the backend didn't populate. - const int64_t final_tokens = - backend_tokens_generated > 0 ? backend_tokens_generated : ctx.token_count; const char* finish_reason = - (options.max_tokens > 0 && final_tokens >= options.max_tokens) ? "length" : "stop"; + (options.max_tokens > 0 && ctx.token_count >= options.max_tokens) ? "length" : "stop"; dispatch_terminal_once(&ctx, finish_reason, nullptr); const int64_t stream_elapsed = now_ms() - ctx.started_ms; // Tokens/sec over decode time only, not prefill-inclusive wall time. @@ -2306,10 +2300,10 @@ rac_result_t rac_llm_generate_stream_proto(const uint8_t* request_proto_bytes, : stream_elapsed; publish_generation_event(runanywhere::v1::GENERATION_EVENT_KIND_STREAM_COMPLETED, request.prompt().c_str(), nullptr, ctx.response_text.c_str(), - nullptr, ref.model_id, final_tokens, stream_elapsed, + nullptr, ref.model_id, ctx.token_count, stream_elapsed, ctx.prompt_tokens, ref.framework_name, - (final_tokens > 0 && stream_decode > 0) - ? final_tokens * 1000.0 / static_cast(stream_decode) + (ctx.token_count > 0 && stream_decode > 0) + ? ctx.token_count * 1000.0 / static_cast(stream_decode) : 0.0, static_cast(stream_ttft), options.temperature, options.max_tokens, lifecycle_context_length(ref), diff --git a/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp b/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp index f3a84ec3e9..1af7dc93d6 100644 --- a/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp +++ b/sdk/runanywhere-commons/src/features/llm/rac_llm_service.cpp @@ -148,7 +148,7 @@ rac_result_t rac_llm_generate_stream(rac_handle_t handle, const char* prompt, const std::string effective_prompt = rac::llm::apply_no_think_directive(prompt, options ? options->disable_thinking : RAC_FALSE); return service->ops->generate_stream(service->impl, effective_prompt.c_str(), options, callback, - user_data, /*out_tokens_generated=*/nullptr); + user_data); } rac_result_t rac_llm_get_info(rac_handle_t handle, rac_llm_info_t* out_info) { diff --git a/sdk/runanywhere-commons/src/features/llm/structured_output.cpp b/sdk/runanywhere-commons/src/features/llm/structured_output.cpp index ceb53c570b..ac9ae74761 100644 --- a/sdk/runanywhere-commons/src/features/llm/structured_output.cpp +++ b/sdk/runanywhere-commons/src/features/llm/structured_output.cpp @@ -1555,8 +1555,7 @@ rac_structured_output_generate_stream_proto(const uint8_t* request_proto_bytes, // rac_llm_proto_service.cpp generate_stream path. try { rc = ref.ops->generate_stream(ref.impl, prepared_prompt.c_str(), &options, - structured_stream_token_callback, &ctx, - /*out_tokens_generated=*/nullptr); + structured_stream_token_callback, &ctx); } catch (const std::exception& e) { rac_error_set_details(e.what()); rc = RAC_ERROR_INFERENCE_FAILED; From 0b1bc054f5fe6e31b070d4dac188ee73a60f4847 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 14:07:14 +0530 Subject: [PATCH 12/44] Fix PR CI: rebuild proto-ts dist for IDL drift; test C++20 --- sdk/runanywhere-commons/tests/CMakeLists.txt | 2 +- sdk/shared/proto-ts/dist/sdk_events.d.ts | 2 ++ sdk/shared/proto-ts/dist/sdk_events.js | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/sdk/runanywhere-commons/tests/CMakeLists.txt b/sdk/runanywhere-commons/tests/CMakeLists.txt index 9a18183b9c..887c5a4900 100644 --- a/sdk/runanywhere-commons/tests/CMakeLists.txt +++ b/sdk/runanywhere-commons/tests/CMakeLists.txt @@ -577,7 +577,7 @@ target_include_directories(test_telemetry_extraction PRIVATE ) target_link_libraries(test_telemetry_extraction PRIVATE rac_commons) rac_link_archive_deps(test_telemetry_extraction) -target_compile_features(test_telemetry_extraction PRIVATE cxx_std_17) +target_compile_features(test_telemetry_extraction PRIVATE cxx_std_20) rac_test_define_have_protobuf(test_telemetry_extraction) add_test(NAME telemetry_extraction_tests COMMAND test_telemetry_extraction) diff --git a/sdk/shared/proto-ts/dist/sdk_events.d.ts b/sdk/shared/proto-ts/dist/sdk_events.d.ts index 18761e61e1..d75ff3aad0 100644 --- a/sdk/shared/proto-ts/dist/sdk_events.d.ts +++ b/sdk/shared/proto-ts/dist/sdk_events.d.ts @@ -702,6 +702,8 @@ export interface GenerationEvent { durationMs: number; /** InferenceFramework enum int */ framework: number; + /** prompt eval (prefill) duration */ + promptEvalTimeMs: number; } /** * --------------------------------------------------------------------------- diff --git a/sdk/shared/proto-ts/dist/sdk_events.js b/sdk/shared/proto-ts/dist/sdk_events.js index de88c059a2..c567a440c9 100644 --- a/sdk/shared/proto-ts/dist/sdk_events.js +++ b/sdk/shared/proto-ts/dist/sdk_events.js @@ -4026,6 +4026,7 @@ function createBaseGenerationEvent() { modelName: "", durationMs: 0, framework: 0, + promptEvalTimeMs: 0, }; } exports.GenerationEvent = { @@ -4129,6 +4130,9 @@ exports.GenerationEvent = { if (message.framework !== 0) { writer.uint32(264).int32(message.framework); } + if (message.promptEvalTimeMs !== 0) { + writer.uint32(272).int64(message.promptEvalTimeMs); + } return writer; }, decode(input, length) { @@ -4369,6 +4373,13 @@ exports.GenerationEvent = { message.framework = reader.int32(); continue; } + case 34: { + if (tag !== 272) { + break; + } + message.promptEvalTimeMs = longToNumber(reader.int64()); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -4516,6 +4527,11 @@ exports.GenerationEvent = { ? globalThis.Number(object.duration_ms) : 0, framework: isSet(object.framework) ? globalThis.Number(object.framework) : 0, + promptEvalTimeMs: isSet(object.promptEvalTimeMs) + ? globalThis.Number(object.promptEvalTimeMs) + : isSet(object.prompt_eval_time_ms) + ? globalThis.Number(object.prompt_eval_time_ms) + : 0, }; }, toJSON(message) { @@ -4619,6 +4635,9 @@ exports.GenerationEvent = { if (message.framework !== 0) { obj.framework = Math.round(message.framework); } + if (message.promptEvalTimeMs !== 0) { + obj.promptEvalTimeMs = Math.round(message.promptEvalTimeMs); + } return obj; }, create(base) { @@ -4659,6 +4678,7 @@ exports.GenerationEvent = { message.modelName = object.modelName ?? ""; message.durationMs = object.durationMs ?? 0; message.framework = object.framework ?? 0; + message.promptEvalTimeMs = object.promptEvalTimeMs ?? 0; return message; }, }; From 0ffe1089e9d0b71c4d54963b10cafb2ccf320a94 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 19 Jul 2026 15:20:53 +0530 Subject: [PATCH 13/44] Fix PR CI: gate rcli rag command on RAC_BACKEND_RAG (Windows); ktlint format web search tool --- sdk/runanywhere-cli/CMakeLists.txt | 6 +++ sdk/runanywhere-cli/src/commands/cmd_rag.cpp | 18 ++++++++ .../LLM/RunAnywhereWebSearchTool.kt | 41 ++++++++++++------- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/sdk/runanywhere-cli/CMakeLists.txt b/sdk/runanywhere-cli/CMakeLists.txt index 9db6ae4667..28a1a2695f 100644 --- a/sdk/runanywhere-cli/CMakeLists.txt +++ b/sdk/runanywhere-cli/CMakeLists.txt @@ -111,6 +111,12 @@ if(NOT RAC_PROTOBUF_RUNTIME_ENABLED "for the model-lifecycle proto ABI") endif() target_compile_definitions(rcli_core PUBLIC RAC_HAVE_PROTOBUF=1) +# The RAG pipeline is folded into rac_commons only when RAC_BACKEND_RAG is ON +# (it is OFF on the Windows CLI preset). rac_commons keeps RAC_HAVE_RAG PRIVATE, +# so re-attach it here to gate the `rag` command's rac_rag_*_proto references. +if(RAC_BACKEND_RAG) + target_compile_definitions(rcli_core PUBLIC RAC_HAVE_RAG=1) +endif() if(RAC_PROTOBUF_NAMESPACE_ISOLATED) # Generated messages and the statically bundled runtime must use the same # private namespace token rewrite. rac_commons keeps this definition diff --git a/sdk/runanywhere-cli/src/commands/cmd_rag.cpp b/sdk/runanywhere-cli/src/commands/cmd_rag.cpp index 1648f2522f..6badc279ce 100644 --- a/sdk/runanywhere-cli/src/commands/cmd_rag.cpp +++ b/sdk/runanywhere-cli/src/commands/cmd_rag.cpp @@ -17,6 +17,22 @@ #include "commands/commands.h" +#if !defined(RAC_HAVE_RAG) + +// The RAG pipeline is not folded into this binary (RAC_BACKEND_RAG=OFF, e.g. the +// Windows CLI preset), so the rac_rag_*_proto symbols are unavailable. Register +// no `rag` subcommand rather than fail to link. +namespace rcli::commands { + +void register_rag(CLI::App& app, GlobalOptions& options) { + (void)app; + (void)options; +} + +} // namespace rcli::commands + +#else + #include #include #include @@ -219,3 +235,5 @@ void register_rag(CLI::App& app, GlobalOptions& options) { } } // namespace rcli::commands + +#endif // RAC_HAVE_RAG diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt index a52066b489..a3b31a4468 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereWebSearchTool.kt @@ -77,7 +77,8 @@ private object WebSearchTool { private const val SNIPPET_TAIL_CHARS = 1_500 private val client = - OkHttpClient.Builder() + OkHttpClient + .Builder() .callTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) @@ -185,7 +186,8 @@ private object WebSearchTool { private suspend fun httpGet(url: HttpUrl): String = withContext(Dispatchers.IO) { val request = - Request.Builder() + Request + .Builder() .url(url) .header(USER_AGENT_HEADER, USER_AGENT) .build() @@ -226,15 +228,17 @@ private object WebSearchTool { } private fun parseLiteResults(html: String): List = - resultLinkRegex.findAll(html).mapNotNull { match -> - val href = match.groupValues.getOrNull(1)?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null - val rawTitle = match.groupValues.getOrNull(2) ?: return@mapNotNull null - val resolvedURL = redirectURL(decodeHTML(href)) - val cleanTitle = cleanHTML(rawTitle) - if (cleanTitle.isEmpty() || resolvedURL.isEmpty()) return@mapNotNull null - val snippet = snippetAfter(match.range.last + 1, html) ?: cleanTitle - SearchResult(title = cleanTitle, url = resolvedURL, snippet = snippet) - }.toList() + resultLinkRegex + .findAll(html) + .mapNotNull { match -> + val href = match.groupValues.getOrNull(1)?.takeIf { it.isNotEmpty() } ?: return@mapNotNull null + val rawTitle = match.groupValues.getOrNull(2) ?: return@mapNotNull null + val resolvedURL = redirectURL(decodeHTML(href)) + val cleanTitle = cleanHTML(rawTitle) + if (cleanTitle.isEmpty() || resolvedURL.isEmpty()) return@mapNotNull null + val snippet = snippetAfter(match.range.last + 1, html) ?: cleanTitle + SearchResult(title = cleanTitle, url = resolvedURL, snippet = snippet) + }.toList() private fun snippetAfter(startIndex: Int, html: String): String? { if (startIndex >= html.length) return null @@ -295,7 +299,8 @@ private object WebSearchTool { private fun makeLiteSearchURL(query: String): HttpUrl? = runCatching { - HttpUrl.Builder() + HttpUrl + .Builder() .scheme("https") .host("lite.duckduckgo.com") .addPathSegment("lite") @@ -306,7 +311,8 @@ private object WebSearchTool { private fun makeInstantAnswerURL(query: String): HttpUrl? = runCatching { - HttpUrl.Builder() + HttpUrl + .Builder() .scheme("https") .host("api.duckduckgo.com") .addQueryParameter("q", query) @@ -319,7 +325,8 @@ private object WebSearchTool { private fun makeSearchResultsURL(query: String): HttpUrl? = runCatching { - HttpUrl.Builder() + HttpUrl + .Builder() .scheme("https") .host("duckduckgo.com") .addPathSegment("") @@ -336,7 +343,11 @@ private object WebSearchTool { ToolValue(object_value = ToolValueObject(fields = fields)) private fun JsonObject.stringField(key: String): String = - (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.content?.trim().orEmpty() + (this[key] as? JsonPrimitive) + ?.takeIf { it.isString } + ?.content + ?.trim() + .orEmpty() private val resultLinkRegex = Regex( From 111ad36f87f402e275a7d1626457aa1a463bcdf1 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Tue, 21 Jul 2026 17:21:56 +0530 Subject: [PATCH 14/44] =?UTF-8?q?staging:=20keyless=20environment=20with?= =?UTF-8?q?=20baked=20backend=20URL=20=E2=80=94=20staging=20overrides=20ca?= =?UTF-8?q?ller=20URL/key=20in=20commons=20(state,=20sdk-config,=20phase1)?= =?UTF-8?q?;=20validation=20and=20telemetry=20flush=20accept=20keyless?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/runanywhere-commons/CMakeLists.txt | 4 ++ .../infrastructure/network/rac_dev_config.h | 11 +++++ .../infrastructure/network/rac_environment.h | 13 ++++++ .../src/core/sdk_state.cpp | 11 +++++ .../network/development_config.cpp.template | 8 ++++ .../infrastructure/network/environment.cpp | 40 +++++++++++++++++-- .../telemetry/telemetry_manager.cpp | 36 ++++------------- .../src/lifecycle/sdk_init.cpp | 36 ++++++++++++++--- 8 files changed, 121 insertions(+), 38 deletions(-) diff --git a/sdk/runanywhere-commons/CMakeLists.txt b/sdk/runanywhere-commons/CMakeLists.txt index e5900018e6..8571dcb6da 100644 --- a/sdk/runanywhere-commons/CMakeLists.txt +++ b/sdk/runanywhere-commons/CMakeLists.txt @@ -691,6 +691,10 @@ else() else() message(STATUS "Using credential-free development config stub") endif() + if(NOT "$ENV{STAGING_BASE_URL}" STREQUAL "") + string(REPLACE "YOUR_STAGING_BASE_URL" "$ENV{STAGING_BASE_URL}" _rac_dev_cfg "${_rac_dev_cfg}") + message(STATUS "Injected staging base URL from environment (CI secret)") + endif() file(WRITE "${RAC_DEV_CONFIG_SOURCE}" "${_rac_dev_cfg}") endif() diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h index 9541c386c8..2e8254fac3 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h @@ -58,6 +58,17 @@ RAC_API const char* rac_dev_config_get_supabase_key(void); */ RAC_API const char* rac_dev_config_get_build_token(void); +/** + * @brief Get the baked staging backend base URL + * + * Team builds bake the staging URL via the git-ignored development_config.cpp + * so callers can init with environment=staging and nothing else. Open-source + * builds keep the placeholder and must pass a base URL explicitly. + * + * @return URL string or placeholder (static, do not free) + */ +RAC_API const char* rac_dev_config_get_staging_base_url(void); + // ============================================================================= // Convenience Functions // ============================================================================= diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h index d05ad32cba..a103b60391 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h @@ -83,6 +83,19 @@ typedef struct { */ RAC_API bool rac_env_requires_auth(rac_environment_t env); +/** + * @brief Check whether authenticated requests are expected for this config + * + * Staging accepts keyless clients: with no API key configured, requests go + * out unauthenticated and the backend attributes them to the PUBLIC org. + * Production always expects auth; development never does. + * + * @param env The environment to check + * @param api_key The configured API key (may be NULL or empty) + * @return true when the SDK must authenticate before talking to the backend + */ +RAC_API bool rac_env_auth_expected(rac_environment_t env, const char* api_key); + /** * @brief Check if environment requires a backend URL * @param env The environment to check diff --git a/sdk/runanywhere-commons/src/core/sdk_state.cpp b/sdk/runanywhere-commons/src/core/sdk_state.cpp index bc4ab04564..a73e75448f 100644 --- a/sdk/runanywhere-commons/src/core/sdk_state.cpp +++ b/sdk/runanywhere-commons/src/core/sdk_state.cpp @@ -19,6 +19,7 @@ #include "rac/core/rac_logger.h" #include "rac/core/rac_sdk_state.h" #include "rac/infrastructure/events/rac_sdk_event_stream.h" +#include "rac/infrastructure/network/rac_dev_config.h" // ============================================================================= // Internal C++ State Class @@ -49,6 +50,16 @@ class SDKState { environment_ = env; api_key_ = api_key ? api_key : ""; base_url_ = base_url ? base_url : ""; + // Staging is absolute: whatever the caller passed, requests go keyless + // to the baked staging backend (git-ignored dev config / CI secret). + // Builds without the baked URL keep the caller's URL as-is. + if (env == RAC_ENV_STAGING) { + api_key_.clear(); + const char* baked = rac_dev_config_get_staging_base_url(); + if (rac_dev_config_is_usable_http_url(baked)) { + base_url_ = baked; + } + } device_id_ = device_id ? device_id : ""; is_initialized_ = true; return RAC_SUCCESS; diff --git a/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template b/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template index 7e1443a3b9..d276c3fc62 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template +++ b/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template @@ -38,6 +38,10 @@ constexpr const char* SUPABASE_ANON_KEY = "YOUR_SUPABASE_ANON_KEY"; // Get this from your team's credential storage, or use a debug token for local dev constexpr const char* BUILD_TOKEN = "YOUR_BUILD_TOKEN"; +// Staging backend base URL — baked into team builds so environment=staging +// needs no explicit URL. Leave the placeholder to require an explicit URL. +constexpr const char* STAGING_BASE_URL = "YOUR_STAGING_BASE_URL"; + std::string trim(const char* value) { if (!value) { return {}; @@ -122,6 +126,10 @@ const char* rac_dev_config_get_build_token(void) { return BUILD_TOKEN; } +const char* rac_dev_config_get_staging_base_url(void) { + return STAGING_BASE_URL; +} + bool rac_dev_config_has_supabase(void) { return is_usable_http_url(SUPABASE_URL) && !looks_like_placeholder(SUPABASE_ANON_KEY); } diff --git a/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp b/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp index da5a42b2d3..557d4b6515 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp @@ -9,6 +9,7 @@ #include "rac/core/rac_logger.h" #include "rac/core/rac_types.h" +#include "rac/infrastructure/network/rac_dev_config.h" #include "rac/infrastructure/network/rac_environment.h" // ============================================================================= @@ -63,6 +64,18 @@ bool rac_env_requires_auth(rac_environment_t env) { return env != RAC_ENV_DEVELOPMENT; } +bool rac_env_auth_expected(rac_environment_t env, const char* api_key) { + if (!rac_env_requires_auth(env)) { + return false; + } + // Staging accepts keyless clients — requests go out unauthenticated and + // the backend attributes them to the PUBLIC org. Production stays strict. + if (env == RAC_ENV_STAGING && (!api_key || api_key[0] == '\0')) { + return false; + } + return true; +} + bool rac_env_requires_backend_url(rac_environment_t env) { return env != RAC_ENV_DEVELOPMENT; } @@ -196,12 +209,13 @@ static bool is_localhost_host(const char* host) { // ============================================================================= rac_validation_result_t rac_validate_api_key(const char* api_key, rac_environment_t env) { - // Development mode doesn't require API key - if (!rac_env_requires_auth(env)) { + // Development never needs a key; staging accepts an empty one (keyless + // clients send unauthenticated requests, attributed to the PUBLIC org) + if (!rac_env_auth_expected(env, api_key)) { return RAC_VALIDATION_OK; } - // Staging/Production require API key + // Production requires API key if (!api_key || api_key[0] == '\0') { return RAC_VALIDATION_API_KEY_REQUIRED; } @@ -220,8 +234,13 @@ rac_validation_result_t rac_validate_base_url(const char* url, rac_environment_t return RAC_VALIDATION_OK; } - // Staging/Production require URL + // Staging/Production require URL — except staging builds carrying the + // baked backend URL, where an empty URL resolves to it at init if (!url || url[0] == '\0') { + if (env == RAC_ENV_STAGING && + rac_dev_config_is_usable_http_url(rac_dev_config_get_staging_base_url())) { + return RAC_VALIDATION_OK; + } return RAC_VALIDATION_URL_REQUIRED; } @@ -397,6 +416,19 @@ rac_validation_result_t rac_sdk_init(const rac_sdk_config_t* config) { return RAC_VALIDATION_API_KEY_REQUIRED; } + // Staging is absolute: whatever the caller passed, requests go keyless to + // the baked staging backend (git-ignored dev config / CI secret). Builds + // without the baked URL keep the caller's URL as-is. + rac_sdk_config_t effective = *config; + if (effective.environment == RAC_ENV_STAGING) { + effective.api_key = ""; + const char* baked = rac_dev_config_get_staging_base_url(); + if (rac_dev_config_is_usable_http_url(baked)) { + effective.base_url = baked; + } + } + config = &effective; + // Validate configuration rac_validation_result_t result = rac_validate_config(config); if (result != RAC_VALIDATION_OK) { diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp index 25bb4a4fe7..5213ccd873 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp @@ -17,6 +17,7 @@ #include #include "rac/core/rac_logger.h" +#include "rac/core/rac_sdk_state.h" #include "rac/infrastructure/network/rac_auth_manager.h" #include "rac/infrastructure/network/rac_endpoints.h" #include "rac/infrastructure/network/rac_environment.h" @@ -927,7 +928,6 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.time_to_first_token_ms = g.time_to_first_token_ms() != 0 ? static_cast(g.time_to_first_token_ms()) : static_cast(g.first_token_latency_ms()); - payload.prompt_eval_time_ms = static_cast(g.prompt_eval_time_ms()); payload.is_streaming = g.is_streaming() ? RAC_TRUE : RAC_FALSE; payload.has_is_streaming = RAC_TRUE; framework_str = framework_proto_to_string(g.framework()); @@ -1174,10 +1174,6 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, if (ttft_it != ev.properties().end()) { payload.time_to_first_token_ms = std::atof(ttft_it->second.c_str()); } - auto pe_it = ev.properties().find("prompt_eval_time_ms"); - if (pe_it != ev.properties().end()) { - payload.prompt_eval_time_ms = std::atof(pe_it->second.c_str()); - } auto temp_it = ev.properties().find("temperature"); if (temp_it != ev.properties().end()) { payload.temperature = std::atof(temp_it->second.c_str()); @@ -1228,16 +1224,6 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.reranker_used = rr_it->second == "1" ? RAC_TRUE : RAC_FALSE; payload.has_reranker_used = RAC_TRUE; } - auto qt_it = ev.properties().find("query_token_count"); - if (qt_it != ev.properties().end()) { - payload.query_token_count = - static_cast(std::atoi(qt_it->second.c_str())); - } - auto ct_it = ev.properties().find("context_tokens"); - if (ct_it != ev.properties().end()) { - payload.context_tokens = - static_cast(std::atoi(ct_it->second.c_str())); - } break; } case runanywhere::v1::SDK_COMPONENT_EMBEDDINGS: { @@ -1245,23 +1231,12 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, // embedding_model is read from model_id (set above) in the JSON. payload.input_count = static_cast(c.input_count()); payload.vectors_produced = static_cast(c.output_count()); - // embedding_dimension / total_tokens / batch_size ride the - // properties carrier (no CapabilityOperationEvent fields). + // embedding_dimension rides the properties carrier (no proto field). auto dim_it = ev.properties().find("embedding_dimension"); if (dim_it != ev.properties().end()) { payload.embedding_dimension = static_cast(std::atoi(dim_it->second.c_str())); } - auto tok_it = ev.properties().find("total_tokens"); - if (tok_it != ev.properties().end()) { - payload.total_tokens = - static_cast(std::atoi(tok_it->second.c_str())); - } - auto bs_it = ev.properties().find("batch_size"); - if (bs_it != ev.properties().end()) { - payload.batch_size = - static_cast(std::atoi(bs_it->second.c_str())); - } break; } case runanywhere::v1::SDK_COMPONENT_DIFFUSION: { @@ -1463,8 +1438,11 @@ rac_result_t rac_telemetry_manager_flush(rac_telemetry_manager_t* manager) { // The V2 telemetry endpoints only accept a JWT; flushing before // authentication would 401 and silently drop the batch (the HTTP callback // is fire-and-forget). Keep events queued — rac_auth_handle_*_response - // kicks a flush the moment a token lands. - if (rac_env_requires_auth(manager->environment) && !rac_auth_is_authenticated()) { + // kicks a flush the moment a token lands. Keyless staging never expects a + // token: events flush unauthenticated and the backend attributes them to + // the PUBLIC org. + if (rac_env_auth_expected(manager->environment, rac_state_get_api_key()) && + !rac_auth_is_authenticated()) { size_t queued = 0; { std::lock_guard lock(manager->queue_mutex); diff --git a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp index c7cfc8425a..d7f85e0dae 100644 --- a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp +++ b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp @@ -117,8 +117,11 @@ bool http_setup_applicable_for_state() { const char* api_key = rac_state_get_api_key(); const char* base_url = rac_state_get_base_url(); - return rac_dev_config_is_usable_http_url(base_url) && - rac_dev_config_is_usable_credential(api_key); + if (!rac_dev_config_is_usable_http_url(base_url)) { + return false; + } + // Keyless staging is a valid HTTP setup (unauthenticated public ingestion) + return rac_dev_config_is_usable_credential(api_key) || !rac_env_auth_expected(env, api_key); } std::string warning_from_code(const char* prefix, rac_result_t code) { @@ -311,7 +314,19 @@ rac_result_t perform_authentication(SdkInitResult* result) { const char* api_key = rac_state_get_api_key(); const char* base_url = rac_state_get_base_url(); - if (!has_nonempty_string(api_key) || !has_nonempty_string(base_url)) { + if (!has_nonempty_string(base_url)) { + result->set_http_configured(false); + result->set_has_completed_http_setup(false); + return RAC_ERROR_INVALID_CONFIGURATION; + } + if (!rac_env_auth_expected(env, api_key)) { + // Keyless staging: requests go out unauthenticated (public ingestion), + // there is no token to fetch + result->set_http_configured(rac_http_transport_is_registered() == RAC_TRUE); + result->set_has_completed_http_setup(true); + return RAC_SUCCESS; + } + if (!has_nonempty_string(api_key)) { result->set_http_configured(false); result->set_has_completed_http_setup(false); return RAC_ERROR_INVALID_CONFIGURATION; @@ -520,9 +535,20 @@ rac_result_t rac_sdk_init_phase1_proto(const uint8_t* in_request_bytes, size_t i } const rac_environment_t env = to_rac_environment(request.environment()); - const std::string api_key = request.api_key(); - const std::string base_url = request.base_url(); + std::string api_key = request.api_key(); + std::string base_url = request.base_url(); const std::string device_id = request.device_id(); + + // Staging is absolute: whatever the caller passed, requests go keyless to + // the baked staging backend (git-ignored dev config / CI secret). Builds + // without the baked URL keep the caller's URL as-is. + if (env == RAC_ENV_STAGING) { + api_key.clear(); + const char* baked = rac_dev_config_get_staging_base_url(); + if (rac_dev_config_is_usable_http_url(baked)) { + base_url = baked; + } + } const std::string platform = request.platform(); const std::string sdk_version = request.sdk_version().empty() ? std::string(rac_sdk_get_version()) : request.sdk_version(); From e097cd5e3d57dedb331f0a2630157b48f7ff3e71 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Tue, 21 Jul 2026 17:24:54 +0530 Subject: [PATCH 15/44] =?UTF-8?q?tests:=20live=20keyless-staging=20E2E=20?= =?UTF-8?q?=E2=80=94=20env-only=20init=20resolves=20baked=20URL,=20one=20u?= =?UTF-8?q?nauthenticated=20event=20per=20modality=20asserted=20200=20(bui?= =?UTF-8?q?ld-on-demand,=20excluded=20from=20ctest)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/runanywhere-commons/tests/CMakeLists.txt | 12 ++ .../tests/test_staging_keyless_live.cpp | 143 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp diff --git a/sdk/runanywhere-commons/tests/CMakeLists.txt b/sdk/runanywhere-commons/tests/CMakeLists.txt index 887c5a4900..2d50113674 100644 --- a/sdk/runanywhere-commons/tests/CMakeLists.txt +++ b/sdk/runanywhere-commons/tests/CMakeLists.txt @@ -1682,3 +1682,15 @@ endif() include(GoogleTest) gtest_discover_tests(rac_benchmark_tests) + +# --- Keyless staging live E2E (network; build on demand, not in ctest) ------ +# Proves environment=staging with no API key and no base URL resolves the +# baked staging URL and flushes one event per modality unauthenticated. +find_package(CURL QUIET) +if(CURL_FOUND) + add_executable(test_staging_keyless_live EXCLUDE_FROM_ALL test_staging_keyless_live.cpp) + target_include_directories(test_staging_keyless_live PRIVATE ${CMAKE_SOURCE_DIR}/include) + target_link_libraries(test_staging_keyless_live PRIVATE rac_commons CURL::libcurl Threads::Threads) + rac_link_archive_deps(test_staging_keyless_live) + target_compile_features(test_staging_keyless_live PRIVATE cxx_std_17) +endif() diff --git a/sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp b/sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp new file mode 100644 index 0000000000..73d137b6ee --- /dev/null +++ b/sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp @@ -0,0 +1,143 @@ +// Live E2E for keyless staging telemetry: init with environment=staging and +// nothing else (no API key, no base URL — the baked dev-config staging URL +// must resolve), emit one terminal event per modality, flush unauthenticated +// and expect the backend to store each one under the PUBLIC org. +// +// Network test against the real staging backend — build on demand, not part +// of the ctest suite. Requires a build configured with STAGING_BASE_URL (or a +// filled local development_config.cpp). + +#include + +#include +#include +#include +#include +#include +#include + +#include "rac/core/rac_sdk_state.h" +#include "rac/infrastructure/network/rac_environment.h" +#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" +#include "rac/infrastructure/telemetry/rac_telemetry_types.h" + +namespace { + +int g_ok = 0; +int g_failed = 0; + +size_t discard_body(char*, size_t size, size_t nmemb, void*) { + return size * nmemb; +} + +void http_send(void*, const char* endpoint, const char* json_body, size_t json_length, + rac_bool_t requires_auth) { + const std::string url = std::string(rac_state_get_base_url()) + endpoint; + CURL* curl = curl_easy_init(); + if (!curl) { + g_failed++; + return; + } + curl_slist* headers = curl_slist_append(nullptr, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(json_length)); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard_body); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + const CURLcode rc = curl_easy_perform(curl); + long status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + std::printf("POST %s requires_auth=%d -> curl=%d http=%ld\n", endpoint, + static_cast(requires_auth), static_cast(rc), status); + (rc == CURLE_OK && status == 200) ? g_ok++ : g_failed++; + curl_slist_free_all(headers); + curl_easy_cleanup(curl); +} + +std::string random_uuid() { + std::ifstream f("/proc/sys/kernel/random/uuid"); + std::string uuid; + std::getline(f, uuid); + return uuid; +} + +int64_t now_ms() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +} // namespace + +int main() { + rac_state_initialize(RAC_ENV_STAGING, "", "", "keyless-harness-device"); + const char* base_url = rac_state_get_base_url(); + std::printf("resolved base_url: %s\n", base_url && base_url[0] ? base_url : ""); + if (!base_url || base_url[0] == '\0') { + std::printf("FAIL: staging base URL did not resolve from the baked dev config\n"); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + rac_telemetry_manager_t* mgr = + rac_telemetry_manager_create(RAC_ENV_STAGING, "keyless-harness-device", "linux", "0.0.0"); + rac_telemetry_manager_set_device_info(mgr, "Linux Keyless Harness", "6.12"); + rac_telemetry_manager_set_http_callback(mgr, http_send, nullptr); + + struct Case { + const char* modality; + const char* event_type; + }; + const Case cases[] = { + {"llm", "llm.generation.completed"}, + {"stt", "stt.transcription.completed"}, + {"tts", "tts.synthesis.completed"}, + {"vlm", "vlm.generation.completed"}, + {"rag", "rag.retrieval.completed"}, + {"imagegen", "imagegen.generation.completed"}, + {"system", "sdk.init.completed"}, + {"model", "model.download.completed"}, + }; + + for (const Case& c : cases) { + const std::string id = random_uuid(); + rac_telemetry_payload_t p = rac_telemetry_payload_default(); + p.id = id.c_str(); + p.event_type = c.event_type; + p.modality = c.modality; + p.timestamp_ms = now_ms(); + p.created_at_ms = p.timestamp_ms; + p.model_id = "keyless-harness-model"; + p.model_name = "Keyless Harness Model"; + p.framework = "llamacpp"; + p.device = "Linux Keyless Harness"; + p.os_version = "6.12"; + p.platform = "linux"; + p.sdk_version = "0.0.0"; + p.processing_time_ms = 123.0; + p.has_processing_time_ms = RAC_TRUE; + p.success = RAC_TRUE; + p.has_success = RAC_TRUE; + if (std::strcmp(c.modality, "llm") == 0 || std::strcmp(c.modality, "vlm") == 0) { + p.input_tokens = 10; + p.output_tokens = 20; + p.total_tokens = 30; + p.tokens_per_second = 42.0; + } + const rac_result_t rc = rac_telemetry_manager_track(mgr, &p); + if (rc != RAC_SUCCESS) { + std::printf("FAIL: track(%s) rc=%d\n", c.modality, rc); + g_failed++; + } + } + + rac_telemetry_manager_flush(mgr); + std::this_thread::sleep_for(std::chrono::seconds(2)); + rac_telemetry_manager_destroy(mgr); + curl_global_cleanup(); + + std::printf("summary: ok=%d failed=%d\n", g_ok, g_failed); + return g_failed == 0 && g_ok > 0 ? 0 : 1; +} From 010c0438e8e8e5ce3d42cffba94658d001d4cebe Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Tue, 21 Jul 2026 17:25:11 +0530 Subject: [PATCH 16/44] build: --env-staging flag for rcli and android commons builds; STAGING_BASE_URL injected from CI secrets in release workflow --- .github/workflows/release.yml | 3 ++ run | 64 +++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a7ddf0c89..45f97c0a05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,9 @@ env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + # Staging backend base URL baked the same way — lets environment=staging run + # keyless with no explicit URL (see rac_dev_config_get_staging_base_url). + STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} jobs: diff --git a/run b/run index 043c7407db..fe6a5480b8 100755 --- a/run +++ b/run @@ -31,7 +31,9 @@ ${C_BOLD}DOCTOR & SETUP${C_RESET} ${C_DIM}target = android | flutter | rn | ios | web${C_RESET} ${C_BOLD}SDK${C_RESET} - ${C_CMD}sdk commons build-android${C_RESET} Build C++ commons for all Android ABIs + stage .so into consumers + ${C_CMD}sdk commons build-android${C_RESET} ${C_DIM}[--env-staging]${C_RESET} + Build C++ commons for all Android ABIs + stage .so into consumers. + ${C_DIM}--env-staging bakes the staging backend URL (keyless staging)${C_RESET} ${C_CMD}sdk commons build-ios${C_RESET} Build C++ commons xcframework ${C_DIM}(macOS only)${C_RESET} ${C_CMD}sdk commons build-wasm${C_RESET} Build C++ commons for WebAssembly ${C_CMD}sdk commons build-linux${C_RESET} Build C++ commons for the Linux host @@ -58,6 +60,12 @@ ${C_BOLD}EXAMPLE APPS${C_RESET} ${C_CMD}example ios${C_RESET} {build|clean} ${C_DIM}(macOS only)${C_RESET} ${C_CMD}example web${C_RESET} {dev|build|clean} +${C_BOLD}RCLI${C_RESET} + ${C_CMD}rcli build${C_RESET} ${C_DIM}[--env-staging]${C_RESET} Build the desktop CLI (host preset). --env-staging bakes the + staging backend URL from the git-ignored development_config.cpp + so ${C_DIM}RUNANYWHERE_ENVIRONMENT=staging rcli ...${C_RESET} works keyless + ${C_CMD}rcli clean${C_RESET} Wipe rcli build outputs + ${C_BOLD}UTILITIES${C_RESET} ${C_CMD}clean${C_RESET} Clean every SDK + native + cache ${C_CMD}lint${C_RESET} Run linters across all SDKs @@ -90,8 +98,27 @@ require_macos() { [ "$(uname -s)" = "Darwin" ] || die "this target requires macOS" } +# Reads STAGING_BASE_URL out of the git-ignored development_config.cpp so +# --env-staging never needs the URL on the command line (or in this script). +staging_url_from_local_config() { + local cfg="sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp" + [ -f "${cfg}" ] || die "--env-staging needs the git-ignored development_config.cpp (copy the .template next to it and fill STAGING_BASE_URL)" + local url + url="$(sed -n 's/.*STAGING_BASE_URL = "\([^"]*\)".*/\1/p' "${cfg}" | head -1)" + case "${url}" in + ""|YOUR_*) die "STAGING_BASE_URL is not filled in development_config.cpp" ;; + esac + printf '%s' "${url}" +} + cmd_sdk_commons() { - case "${1:-}" in + local sub="${1:-}" + if [ "${2:-}" = "--env-staging" ]; then + STAGING_BASE_URL="$(staging_url_from_local_config)" || exit 1 + export STAGING_BASE_URL + echo "staging build: baking backend URL from local dev config" + fi + case "${sub}" in build-android) bash scripts/build/build-core-android.sh ;; build-ios) require_macos; bash sdk/runanywhere-swift/scripts/build-core-xcframework.sh ;; build-wasm) bash sdk/runanywhere-web/scripts/build-core-wasm.sh ;; @@ -213,6 +240,38 @@ cmd_example() { esac } +cmd_rcli() { + local sub="${1:-}" + shift 2>/dev/null || true + local preset + case "$(uname -s)" in + Darwin) preset="rcli-macos-release" ;; + *) preset="rcli-linux-release" ;; + esac + case "${sub}" in + build) + local extra=() + local arg + for arg in "$@"; do + case "${arg}" in + --env-staging) + # Staging build: bake credentials (incl. STAGING_BASE_URL) from the + # git-ignored development_config.cpp so `RUNANYWHERE_ENVIRONMENT=staging` + # needs no explicit URL. Nothing secret lives in this script. + [ -f sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp ] \ + || die "--env-staging needs the git-ignored development_config.cpp (copy the .template next to it and fill STAGING_BASE_URL)" + extra+=(-DRAC_INCLUDE_LOCAL_DEV_CONFIG=ON) + ;; + *) die "unknown rcli build flag: ${arg}" ;; + esac + done + cmake --preset="${preset}" "${extra[@]}" && cmake --build --preset="${preset}" --target rcli + ;; + clean) rm -rf build/rcli-* ;; + *) bad_subcommand "rcli" "${sub}" ;; + esac +} + cmd_clean() { cmd_sdk_kotlin clean || true cmd_example_android clean || true @@ -237,6 +296,7 @@ case "${1:-help}" in setup) shift; bash scripts/setup/setup.sh "$@" ;; sdk) shift; cmd_sdk "$@" ;; example) shift; cmd_example "$@" ;; + rcli) shift; cmd_rcli "$@" ;; clean) cmd_clean ;; lint) cmd_lint ;; format) cmd_format ;; From 15a6e147a6c3301debf563655709305f8bfd3ef7 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Tue, 21 Jul 2026 17:27:18 +0530 Subject: [PATCH 17/44] sdks: platform HTTP adapters use commons-effective base URL and send keyless staging telemetry unauthenticated (kotlin, swift, flutter, react-native) --- .../runanywhere/lib/public/runanywhere.dart | 36 +++++++++++++------ .../sdk/foundation/bridge/CppBridge.kt | 8 +++-- .../foundation/bridge/HTTPClientAdapter.kt | 30 ++++++++++------ .../core/cpp/bridges/TelemetryBridge.cpp | 29 +++++++++++---- .../packages/core/src/Public/RunAnywhere.ts | 4 ++- .../Foundation/Bridge/HTTPClientAdapter.swift | 26 ++++++++------ .../RunAnywhere/Public/RunAnywhere.swift | 13 ++++--- 7 files changed, 100 insertions(+), 46 deletions(-) diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index 4279bdaa32..8e92386a59 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -103,6 +103,7 @@ import 'package:runanywhere/native/dart_bridge_events.dart'; import 'package:runanywhere/native/dart_bridge_hf_auth.dart'; import 'package:runanywhere/native/dart_bridge_model_registry.dart'; import 'package:runanywhere/native/dart_bridge_sdk_init.dart'; +import 'package:runanywhere/native/dart_bridge_state.dart'; import 'package:runanywhere/native/dart_bridge_telemetry.dart'; import 'package:runanywhere/native/type_conversions/model_types_cpp_bridge.dart' show ProtoInferenceFrameworkCppBridge; @@ -491,27 +492,38 @@ abstract final class RunAnywhere { // RunAnywhere.swift:125-127 (`SDKInitParams(forDevelopmentWithAPIKey:)`). params = SDKInitParams.forDevelopment(apiKey: apiKey ?? ''); } else { - if (apiKey == null || apiKey.isEmpty) { + // Keyless staging is valid: commons overrides the base URL with the + // baked staging backend and requests go out unauthenticated + // (PUBLIC-org ingestion). Production stays strict. + final isStaging = environment == SDKEnvironment.SDK_ENVIRONMENT_STAGING; + if (!isStaging && (apiKey == null || apiKey.isEmpty)) { throw SDKException.validationFailed( 'API key is required for ${environment.description} mode', fieldPath: 'SDKInitParams.apiKey', ); } - if (baseURL == null || baseURL.isEmpty) { + if (!isStaging && (baseURL == null || baseURL.isEmpty)) { throw SDKException.validationFailed( 'Base URL is required for ${environment.description} mode', fieldPath: 'SDKInitParams.baseURL', ); } - final uri = Uri.tryParse(baseURL); - if (uri == null) { - throw SDKException.validationFailed( - 'Invalid base URL: $baseURL', - fieldPath: 'SDKInitParams.baseURL', - ); + final Uri uri; + if (baseURL == null || baseURL.isEmpty) { + // Staging placeholder — replaced by the baked staging URL in commons. + uri = Uri.parse('https://staging.runanywhere.local'); + } else { + final parsed = Uri.tryParse(baseURL); + if (parsed == null) { + throw SDKException.validationFailed( + 'Invalid base URL: $baseURL', + fieldPath: 'SDKInitParams.baseURL', + ); + } + uri = parsed; } params = SDKInitParams( - apiKey: apiKey, + apiKey: apiKey ?? '', baseURL: uri, environment: environment, ); @@ -643,8 +655,12 @@ abstract final class RunAnywhere { static Future _runPhase2(SDKInitParams params, SDKLogger logger) async { // Step 1: Configure the shared HTTP client. Mirrors Swift's inlined // HTTP setup inside `RunAnywhere.performCoreInit()` (no DI container). + // Read the effective base URL from commons state: staging overrides + // whatever the app passed (baked URL, keyless). + final effectiveBaseURL = + DartBridgeState.instance.baseURL ?? params.baseURL.toString(); HTTPClientAdapter.shared.configure( - baseURL: params.baseURL.toString(), + baseURL: effectiveBaseURL, apiKey: params.apiKey, environment: params.environment, ); diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt index bb6879c334..36b4178f8f 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt @@ -393,9 +393,13 @@ object CppBridge { if (_environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { CppBridgeDevConfig.configureHTTP() } else { - val baseUrl = CppBridgeTelemetry.getBaseUrl() + // Read the effective config from commons state: staging + // overrides whatever the app passed (baked URL, keyless). + val baseUrl = RunAnywhereBridge.racStateGetBaseUrl() + ?.takeIf { it.isNotEmpty() } + ?: CppBridgeTelemetry.getBaseUrl() val apiKey = CppBridgeTelemetry.getApiKey() - if (!baseUrl.isNullOrEmpty() && !apiKey.isNullOrEmpty()) { + if (!baseUrl.isNullOrEmpty()) { HTTPClientAdapter.configure(baseUrl, apiKey) true } else { diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt index bcdf39cb71..ad65f5685c 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt @@ -95,7 +95,9 @@ public object HTTPClientAdapter { private data class Configuration( val baseURL: String, - val apiKey: String, + // Null in keyless staging: requests go out unauthenticated and the + // backend attributes them to the PUBLIC org. + val apiKey: String?, ) @Volatile private var configuration: Configuration? = null @@ -108,16 +110,20 @@ public object HTTPClientAdapter { * by Swift's `CppBridge.DevConfig`. On invalid input the adapter is * left unconfigured rather than throwing. */ - public suspend fun configure(baseURL: String, apiKey: String) { - val trimmedKey = apiKey.trim() + public suspend fun configure(baseURL: String, apiKey: String?) { + val trimmedKey = apiKey?.trim() synchronized(stateLock) { - if (!isUsableHTTPURL(baseURL) || !isUsableCredential(trimmedKey)) { + if (!isUsableHTTPURL(baseURL)) { configuration = null logger.info("HTTP adapter not configured: no usable external config") return } - configuration = Configuration(baseURL = baseURL.trimEnd('/'), apiKey = trimmedKey) - logger.info("HTTP adapter configured with base URL: ${urlForLog(baseURL)}") + val usableKey = trimmedKey?.takeIf { isUsableCredential(it) } + configuration = Configuration(baseURL = baseURL.trimEnd('/'), apiKey = usableKey) + logger.info( + "HTTP adapter configured with base URL: ${urlForLog(baseURL)}" + + if (usableKey == null) " (keyless)" else "", + ) } } @@ -139,7 +145,8 @@ public object HTTPClientAdapter { public val hasUsableConfiguration: Boolean get() { val snapshot = configuration ?: return false - return isUsableHTTPURL(snapshot.baseURL) && isUsableCredential(snapshot.apiKey) + // A usable URL is enough — keyless staging sends unauthenticated. + return isUsableHTTPURL(snapshot.baseURL) } // Public request surface @@ -263,12 +270,13 @@ public object HTTPClientAdapter { * - When auth is required and a valid token is available → use it. * - Otherwise fall back to the API key, throwing if none is set. */ - private suspend fun resolveToken(requiresAuth: Boolean, apiKey: String): String { - if (!requiresAuth) return apiKey + private suspend fun resolveToken(requiresAuth: Boolean, apiKey: String?): String { + if (!requiresAuth) return apiKey ?: "" val token = platformResolveAuthToken() if (!token.isNullOrEmpty()) return token - if (apiKey.isNotEmpty()) return apiKey - throw SDKException.authenticationFailed(reason = "No valid authentication token") + // Keyless staging: no token and no key means the request goes out + // unauthenticated (the backend attributes it to the PUBLIC org) + return apiKey ?: "" } /** diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp index c85072101f..495b25c283 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp @@ -17,6 +17,7 @@ #include "ExternalConfigGuard.hpp" #include "rac_dev_config.h" #include "rac_sdk_event_stream.h" // rac_events_set_telemetry_sink +#include "rac_sdk_state.h" // rac_state_get_base_url (staging override) // Platform-specific logging #if defined(ANDROID) || defined(__ANDROID__) @@ -316,9 +317,13 @@ static void telemetryHttpCallback(void *userData, const char *endpoint, apiKey = supabaseConfig.token; LOGD("Telemetry using configured development Supabase endpoint"); } else { - // Production/Staging: Use configured Railway URL - // These come from SDK initialization (App.tsx -> RunAnywhere.initialize) - baseURL = config::trim(InitBridge::shared().getBaseURL()); + // Production/Staging: read the effective URL from commons state — + // staging overrides whatever the app passed (baked URL, keyless) — + // falling back to the SDK-initialization value. + const char *stateURL = rac_state_get_base_url(); + baseURL = (stateURL != nullptr && stateURL[0] != '\0') + ? config::trim(stateURL) + : config::trim(InitBridge::shared().getBaseURL()); // For production mode, prefer JWT access token (from authentication) // over raw API key. This matches Swift/Kotlin behavior. @@ -327,16 +332,26 @@ static void telemetryHttpCallback(void *userData, const char *endpoint, apiKey = accessToken; // Use JWT for Authorization header LOGD("Telemetry using JWT access token"); } else { - // Fallback to API key if not authenticated yet - apiKey = config::trim(InitBridge::shared().getApiKey()); - LOGD("Telemetry using API key (not authenticated)"); + // Fall back to the commons-state key. Staging clears it (keyless): + // the POST goes out with no Authorization header and the backend + // attributes it to the PUBLIC org — a stale app key would 401. + const char *stateKey = rac_state_get_api_key(); + apiKey = config::trim(stateKey != nullptr ? stateKey : ""); + LOGD("Telemetry using %s (not authenticated)", + apiKey.empty() ? "keyless mode" : "API key"); } - if (!config::isUsableHttpUrl(baseURL) || !config::isUsableSecret(apiKey)) { + // Keyless staging is valid: the request goes out unauthenticated and + // the backend attributes it to the PUBLIC org. Only a usable URL is + // mandatory. + if (!config::isUsableHttpUrl(baseURL)) { LOGI("Skipping telemetry/device registration: no usable config"); rac_telemetry_manager_http_complete(manager, RAC_TRUE, "{}", nullptr); return; } + if (!config::isUsableSecret(apiKey)) { + apiKey.clear(); + } LOGD("Telemetry using configured production/staging endpoint"); } diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts index 3486904d25..d4561488a2 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts @@ -242,8 +242,10 @@ export const RunAnywhere = { const effectiveApiKey = isUsableCredential(options.apiKey) ? options.apiKey!.trim() : ''; + // Keyless staging is valid: commons overrides the base URL with the + // baked staging backend and requests go out unauthenticated + // (PUBLIC-org ingestion). Only production demands credentials. const requiresCredentials = - environment === SDKEnvironment.SDK_ENVIRONMENT_STAGING || environment === SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; if ( !isUsableHTTPURL(effectiveBaseURL, { diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift index a844f01ef8..4718165242 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift @@ -29,7 +29,9 @@ public actor HTTPClientAdapter { private struct Configuration: Sendable { let baseURL: URL - let apiKey: String + // Nil in keyless staging: requests go out unauthenticated and the + // backend attributes them to the PUBLIC org. + let apiKey: String? let generation: UInt64 } @@ -53,19 +55,20 @@ public actor HTTPClientAdapter { public func configure(baseURL: URL, apiKey: String) { let trimmedAPIKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) - guard CppBridge.DevConfig.isUsableHTTPURL(baseURL.absoluteString), - CppBridge.DevConfig.isUsableCredential(trimmedAPIKey) else { + guard CppBridge.DevConfig.isUsableHTTPURL(baseURL.absoluteString) else { clearConfiguration() logger.info("HTTP adapter not configured: no usable external config") return } + // Keyless (staging) is valid: requests go out unauthenticated. + let usableKey = CppBridge.DevConfig.isUsableCredential(trimmedAPIKey) ? trimmedAPIKey : nil nextConfigurationGeneration &+= 1 configuration = Configuration( baseURL: baseURL, - apiKey: trimmedAPIKey, + apiKey: usableKey, generation: nextConfigurationGeneration ) - logger.info("HTTP adapter configured with base URL: \(baseURL.host ?? "unknown")") + logger.info("HTTP adapter configured with base URL: \(baseURL.host ?? "unknown")\(usableKey == nil ? " (keyless)" : "")") } public func configure(baseURL: String, apiKey: String) { @@ -80,8 +83,8 @@ public actor HTTPClientAdapter { public var isConfigured: Bool { configuration != nil } public var hasUsableConfiguration: Bool { guard let configuration else { return false } - return CppBridge.DevConfig.isUsableHTTPURL(configuration.baseURL.absoluteString) && - CppBridge.DevConfig.isUsableCredential(configuration.apiKey) + // A usable URL is enough — keyless staging sends unauthenticated. + return CppBridge.DevConfig.isUsableHTTPURL(configuration.baseURL.absoluteString) } /// Clear lifetime-scoped credentials so a later SDK initialization must @@ -169,8 +172,8 @@ public actor HTTPClientAdapter { ) } - private func resolveToken(requiresAuth: Bool, fallbackAPIKey: String) async throws -> String { - if !requiresAuth { return fallbackAPIKey } + private func resolveToken(requiresAuth: Bool, fallbackAPIKey: String?) async throws -> String { + if !requiresAuth { return fallbackAPIKey ?? "" } // `rac_auth_get_valid_token` encodes the "valid → return / expired // → signal refresh" handshake in one call. var tokenPtr: UnsafePointer? @@ -181,8 +184,9 @@ public actor HTTPClientAdapter { status = rac_auth_get_valid_token(&tokenPtr, &needsRefresh) } if status == 0, let ptr = tokenPtr { return String(cString: ptr) } - if !fallbackAPIKey.isEmpty { return fallbackAPIKey } - throw SDKException(code: .authenticationFailed, message: "No valid authentication token", category: .auth) + // Keyless staging: no token and no key means the request goes out + // unauthenticated (the backend attributes it to the PUBLIC org) + return fallbackAPIKey ?? "" } private func clearConfiguration() { diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift index 92a5beb709..39c20313b1 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift @@ -418,11 +418,16 @@ public enum RunAnywhere { } else { logger.debug("HTTP adapter disabled: no usable development config") } - } else if CppBridge.DevConfig.isUsableCredential(params.apiKey), - CppBridge.DevConfig.isUsableHTTPURL(params.baseURL.absoluteString) { - await CppBridge.HTTP.shared.configure(baseURL: params.baseURL, apiKey: params.apiKey) } else { - logger.debug("HTTP adapter disabled: no usable external config") + // Effective config from commons state: staging overrides + // whatever the app passed (baked URL, keyless). + let effectiveURLString = CppBridge.State.baseURL ?? params.baseURL.absoluteString + if CppBridge.DevConfig.isUsableHTTPURL(effectiveURLString), + let effectiveURL = URL(string: effectiveURLString) { + await CppBridge.HTTP.shared.configure(baseURL: effectiveURL, apiKey: params.apiKey) + } else { + logger.debug("HTTP adapter disabled: no usable external config") + } } } From 125956a5fb7600d6bf819feb9cd2c1935ce1b733 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Tue, 21 Jul 2026 17:27:32 +0530 Subject: [PATCH 18/44] rcli: keyless staging boots telemetry (RUNANYWHERE_ENVIRONMENT=staging alone, no key or URL needed) --- sdk/runanywhere-cli/src/bootstrap.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index 6ddaa1fef9..61ba78a9be 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -339,7 +339,11 @@ void initialize_telemetry_auth() { const std::string environment_name = first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); - if (api_key.empty() || base_url.empty()) { + // Keyless staging is valid: the baked staging URL resolves in commons and + // telemetry flushes unauthenticated (PUBLIC-org ingestion). + const bool staging_keyless = + environment_from_name(environment_name) == RAC_ENV_STAGING; + if ((api_key.empty() || base_url.empty()) && !staging_keyless) { return; // Local dev mode — telemetry not sent (staging/prod only). } From ec472f2b7019ef2ffaf17f7cea631e793c453f54 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Tue, 21 Jul 2026 17:27:32 +0530 Subject: [PATCH 19/44] examples: android/flutter/rn apps default to keyless staging for device testing (restore comments inline) --- .../runanywhereai/RunAnywhereApplication.kt | 17 ++++++++++------- .../lib/app/runanywhere_ai_app.dart | 10 ++++++++-- examples/react-native/RunAnywhereAI/App.tsx | 9 ++++++--- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt index 82a3f09143..d65bca17c0 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt @@ -103,11 +103,16 @@ class RunAnywhereApplication : Application() { val hasBackendConfig = BuildConfig.RUNANYWHERE_API_KEY.isNotBlank() && BuildConfig.RUNANYWHERE_BASE_URL.isNotBlank() - val environment = if (hasBackendConfig) { - SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION - } else { - SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - } + // Staging test build: keyless staging — no API key, no URL; the SDK + // resolves the baked staging backend URL and sends unauthenticated + // telemetry (PUBLIC-org ingestion). Restore the config-driven + // selection below to go back to production/development behavior. + val environment = SDKEnvironment.SDK_ENVIRONMENT_STAGING + // val environment = if (hasBackendConfig) { + // SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION + // } else { + // SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT + // } RunAnywhere.initialize( context = this@RunAnywhereApplication, apiKey = BuildConfig.RUNANYWHERE_API_KEY.takeIf { @@ -116,8 +121,6 @@ class RunAnywhereApplication : Application() { baseURL = BuildConfig.RUNANYWHERE_BASE_URL.takeIf { environment == SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION }, - // Configured app builds always use the full production diagnostics tier. - // Unconfigured local builds retain the SDK development fallback. environment = environment, ) // QHexRT (Qualcomm Hexagon NPU). Registration is rejected internally on parts outside diff --git a/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart b/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart index 6cd2c9f912..6c8a8847ad 100644 --- a/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart +++ b/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart @@ -82,8 +82,14 @@ class _RunAnywhereAIAppState extends State { ); debugPrint('✅ SDK initialized with CUSTOM configuration (staging)'); } else { - await RunAnywhere.initialize(); - debugPrint('✅ SDK initialized in DEVELOPMENT mode'); + // Staging test build: keyless staging — no API key, no URL; the SDK + // resolves the baked staging backend URL and sends unauthenticated + // telemetry (PUBLIC-org ingestion). Restore `RunAnywhere.initialize()` + // to go back to development behavior. + await RunAnywhere.initialize( + environment: SDKEnvironment.SDK_ENVIRONMENT_STAGING, + ); + debugPrint('✅ SDK initialized in STAGING mode (keyless)'); } // Re-apply the persisted HuggingFace token (Settings screen) so private diff --git a/examples/react-native/RunAnywhereAI/App.tsx b/examples/react-native/RunAnywhereAI/App.tsx index 8520faec0b..9a9053c491 100644 --- a/examples/react-native/RunAnywhereAI/App.tsx +++ b/examples/react-native/RunAnywhereAI/App.tsx @@ -236,12 +236,15 @@ const App: React.FC = () => { '[App] SDK initialized with backend configuration (staging)' ); } else { + // Staging test build: keyless staging — no API key, no URL; the SDK + // resolves the baked staging backend URL and sends unauthenticated + // telemetry (PUBLIC-org ingestion). Restore DEVELOPMENT to go back. await RunAnywhere.initialize({ apiKey: '', - baseURL: 'https://api.runanywhere.ai', - environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, + baseURL: '', + environment: SDKEnvironment.SDK_ENVIRONMENT_STAGING, }); - console.log('[App] SDK initialized in DEVELOPMENT mode'); + console.log('[App] SDK initialized in STAGING mode (keyless)'); } await registerAll(backendState); From 1d842b29eb215b5bd3e36e0c505e95615390bf06 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Wed, 22 Jul 2026 01:16:30 +0530 Subject: [PATCH 20/44] telemetry: per-event sdk_binding + live device state (battery/RAM/CPU) with push model for isolate-bound bridges, HTTP retry with backoff, auth-driven registration heal, nested registration everywhere --- sdk/runanywhere-cli/CMakeLists.txt | 1 + sdk/runanywhere-cli/src/bootstrap.cpp | 6 +- sdk/runanywhere-cli/src/device_info.cpp | 595 ++++++++++++++++++ sdk/runanywhere-cli/src/device_info.h | 15 + sdk/runanywhere-commons/CMakeLists.txt | 1 + .../telemetry/rac_telemetry_types.h | 32 +- .../src/desktop/desktop_adapter.cpp | 4 + .../src/infrastructure/device/cpu_state.cpp | 211 +++++++ .../device/rac_device_live_state_internal.h | 84 +++ .../device/rac_device_manager.cpp | 66 +- .../infrastructure/network/auth_manager.cpp | 23 + .../src/infrastructure/network/endpoints.cpp | 3 +- .../infrastructure/network/environment.cpp | 3 +- .../telemetry/telemetry_json.cpp | 70 +-- .../telemetry/telemetry_manager.cpp | 284 ++++++++- .../telemetry/telemetry_types.cpp | 4 + .../src/jni/runanywhere_commons_jni.cpp | 4 + .../src/lifecycle/sdk_init.cpp | 8 + .../tests/test_telemetry_extraction.cpp | 73 ++- 19 files changed, 1386 insertions(+), 101 deletions(-) create mode 100644 sdk/runanywhere-cli/src/device_info.cpp create mode 100644 sdk/runanywhere-cli/src/device_info.h create mode 100644 sdk/runanywhere-commons/src/infrastructure/device/cpu_state.cpp create mode 100644 sdk/runanywhere-commons/src/infrastructure/device/rac_device_live_state_internal.h diff --git a/sdk/runanywhere-cli/CMakeLists.txt b/sdk/runanywhere-cli/CMakeLists.txt index 28a1a2695f..b64ebd574b 100644 --- a/sdk/runanywhere-cli/CMakeLists.txt +++ b/sdk/runanywhere-cli/CMakeLists.txt @@ -57,6 +57,7 @@ set(RCLI_SOURCES src/commands/engine_options.cpp src/commands/model_setup.cpp src/config/cli_paths.cpp + src/device_info.cpp src/io/wav_io.cpp src/io/image_io.cpp src/io/output.cpp diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index 61ba78a9be..1f5925ffef 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -30,6 +30,7 @@ #include "catalog/catalog.h" #include "config/cli_paths.h" +#include "device_info.h" #include "io/output.h" #if defined(RCLI_HAS_LLAMACPP) @@ -211,7 +212,7 @@ void initialize_sdk_metadata() { sdk_config.device_id = device_id[0] != '\0' ? device_id : ""; sdk_config.platform = desktop_platform(); sdk_config.sdk_version = RCLI_VERSION; - sdk_config.client_info.sdk_binding = "rcli"; + sdk_config.client_info.sdk_binding = "cli"; sdk_config.client_info.app_identifier = "ai.runanywhere.rcli"; sdk_config.client_info.app_name = "RunAnywhere CLI"; sdk_config.client_info.app_version = RCLI_VERSION; @@ -490,6 +491,9 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { } initialize_sdk_metadata(); + if (install_device_callbacks() != RAC_SUCCESS) { + out::status_line("warning: device info callbacks failed to register"); + } initialize_telemetry_auth(); #if defined(RCLI_HAS_LLAMACPP) diff --git a/sdk/runanywhere-cli/src/device_info.cpp b/sdk/runanywhere-cli/src/device_info.cpp new file mode 100644 index 0000000000..3c7253dbb4 --- /dev/null +++ b/sdk/runanywhere-cli/src/device_info.cpp @@ -0,0 +1,595 @@ +#include "device_info.h" + +#include +#include +#include +#include + +#include "rac/core/rac_sdk_state.h" +#include "rac/foundation/rac_sha256.h" +#include "rac/infrastructure/device/rac_device_identity.h" +#include "rac/infrastructure/device/rac_device_manager.h" +#include "rac/infrastructure/http/rac_http_client.h" +#include "rac/infrastructure/http/rac_http_transport.h" +#include "rac/infrastructure/network/rac_auth_manager.h" +#include "rac/infrastructure/network/rac_endpoints.h" +#include "rac/infrastructure/network/rac_environment.h" + +#if defined(_WIN32) +#include +#elif defined(__APPLE__) +#include +#include +#include +#else +#include +#include + +#include +#include +#endif + +namespace rcli { + +namespace { + +struct DeviceInfoState { + std::string device_id; + std::string model; + std::string name; + std::string platform; + std::string os_version; + std::string form_factor; + std::string architecture; + std::string chip; + std::string gpu_family; + std::string battery_state; + std::string fingerprint; + double battery_level = -1.0; + int64_t total_memory = 0; + int64_t available_memory = 0; + int32_t core_count = 0; + int32_t performance_cores = 0; + int32_t efficiency_cores = 0; + bool registered = false; + std::string http_body; + std::string http_error; +}; + +DeviceInfoState &state() { + static DeviceInfoState s; + return s; +} + +std::string trim(const std::string &value) { + const char *ws = " \t\r\n"; + const std::size_t begin = value.find_first_not_of(ws); + if (begin == std::string::npos) { + return {}; + } + const std::size_t end = value.find_last_not_of(ws); + return value.substr(begin, end - begin + 1); +} + +#if !defined(_WIN32) && !defined(__APPLE__) + +std::string read_first_line(const std::string &path) { + std::ifstream file(path); + std::string line; + if (file.is_open() && std::getline(file, line)) { + return trim(line); + } + return {}; +} + +std::string os_release_pretty_name() { + std::ifstream file("/etc/os-release"); + std::string line; + while (file.is_open() && std::getline(file, line)) { + const std::string key = "PRETTY_NAME="; + if (line.compare(0, key.size(), key) == 0) { + std::string value = trim(line.substr(key.size())); + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { + value = value.substr(1, value.size() - 2); + } + return value; + } + } + return {}; +} + +std::string cpuinfo_model_name() { + std::ifstream file("/proc/cpuinfo"); + std::string line; + while (file.is_open() && std::getline(file, line)) { + if (line.compare(0, 10, "model name") == 0 || + line.compare(0, 8, "Hardware") == 0) { + const std::size_t colon = line.find(':'); + if (colon != std::string::npos) { + return trim(line.substr(colon + 1)); + } + } + } + return {}; +} + +int64_t meminfo_bytes(const char *key) { + std::ifstream file("/proc/meminfo"); + std::string line; + const std::string prefix = std::string(key) + ":"; + while (file.is_open() && std::getline(file, line)) { + if (line.compare(0, prefix.size(), prefix) == 0) { + const int64_t kib = std::strtoll(line.c_str() + prefix.size(), nullptr, 10); + return kib > 0 ? kib * 1024 : 0; + } + } + return 0; +} + +bool has_battery_dir(std::string *battery_path) { + namespace fs = std::filesystem; + std::error_code ec; + for (const auto &entry : fs::directory_iterator("/sys/class/power_supply", ec)) { + const std::string name = entry.path().filename().string(); + if (name.compare(0, 3, "BAT") == 0) { + if (battery_path) { + *battery_path = entry.path().string(); + } + return true; + } + } + return false; +} + +std::string linux_gpu_family() { + std::error_code ec; + if (std::filesystem::exists("/proc/driver/nvidia/version", ec)) { + return "nvidia"; + } + namespace fs = std::filesystem; + for (const auto &entry : fs::directory_iterator("/sys/class/drm", ec)) { + const std::string card = entry.path().filename().string(); + if (card.compare(0, 4, "card") != 0) { + continue; + } + std::ifstream uevent(entry.path() / "device/uevent"); + std::string line; + while (uevent.is_open() && std::getline(uevent, line)) { + if (line.compare(0, 7, "DRIVER=") != 0) { + continue; + } + const std::string driver = trim(line.substr(7)); + if (driver == "amdgpu" || driver == "radeon") { + return "amd"; + } + if (driver == "i915" || driver == "xe") { + return "intel"; + } + if (driver == "nvidia" || driver == "nouveau") { + return "nvidia"; + } + } + } + return "unknown"; +} + +void linux_core_topology(int32_t core_count, int32_t *perf, int32_t *eff) { + std::vector max_freqs; + max_freqs.reserve(static_cast(core_count)); + int64_t highest = 0; + for (int32_t cpu = 0; cpu < core_count; ++cpu) { + const std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(cpu) + + "/cpufreq/cpuinfo_max_freq"; + const std::string value = read_first_line(path); + const int64_t freq = value.empty() ? 0 : std::strtoll(value.c_str(), nullptr, 10); + if (freq <= 0) { + *perf = core_count; + *eff = 0; + return; + } + max_freqs.push_back(freq); + highest = freq > highest ? freq : highest; + } + int32_t performance = 0; + for (const int64_t freq : max_freqs) { + if (freq == highest) { + ++performance; + } + } + if (performance == 0 || performance == core_count) { + *perf = core_count; + *eff = 0; + return; + } + *perf = performance; + *eff = core_count - performance; +} + +void collect_device_info(DeviceInfoState &info) { + info.platform = "linux"; + + info.model = read_first_line("/sys/devices/virtual/dmi/id/product_name"); + if (info.model.empty()) { + info.model = "Linux Desktop"; + } + + info.name = read_first_line("/etc/hostname"); + if (info.name.empty()) { + char hostname[256] = {}; + if (gethostname(hostname, sizeof(hostname) - 1) == 0 && hostname[0] != '\0') { + info.name = hostname; + } + } + if (info.name.empty()) { + info.name = info.model; + } + + info.os_version = os_release_pretty_name(); + if (info.os_version.empty()) { + info.os_version = "Linux"; + } + + info.chip = cpuinfo_model_name(); + if (info.chip.empty()) { + info.chip = "unknown"; + } + + info.total_memory = meminfo_bytes("MemTotal"); + info.available_memory = meminfo_bytes("MemAvailable"); + + const long online = sysconf(_SC_NPROCESSORS_ONLN); + info.core_count = online > 0 ? static_cast(online) : 1; + linux_core_topology(info.core_count, &info.performance_cores, + &info.efficiency_cores); + + struct utsname uts = {}; + info.architecture = (uname(&uts) == 0 && uts.machine[0] != '\0') + ? uts.machine + : "unknown"; + + std::string battery_path; + if (has_battery_dir(&battery_path)) { + info.form_factor = "laptop"; + const std::string capacity = read_first_line(battery_path + "/capacity"); + if (!capacity.empty()) { + const long percent = std::strtol(capacity.c_str(), nullptr, 10); + if (percent >= 0 && percent <= 100) { + info.battery_level = static_cast(percent) / 100.0; + } + } + const std::string status = read_first_line(battery_path + "/status"); + if (info.battery_level >= 0.0 && !status.empty()) { + if (status == "Full") { + info.battery_state = "full"; + } else if (status == "Charging") { + info.battery_state = "charging"; + } else { + info.battery_state = "unplugged"; + } + } + } else { + info.form_factor = "desktop"; + } + + info.gpu_family = linux_gpu_family(); +} + +#elif defined(__APPLE__) + +std::string sysctl_string(const char *key) { + std::size_t size = 0; + if (sysctlbyname(key, nullptr, &size, nullptr, 0) != 0 || size == 0) { + return {}; + } + std::string value(size, '\0'); + if (sysctlbyname(key, value.data(), &size, nullptr, 0) != 0) { + return {}; + } + value.resize(value.find('\0') != std::string::npos ? value.find('\0') + : value.size()); + return trim(value); +} + +int64_t sysctl_i64(const char *key) { + int64_t value = 0; + std::size_t size = sizeof(value); + if (sysctlbyname(key, &value, &size, nullptr, 0) != 0) { + return 0; + } + return value; +} + +void collect_device_info(DeviceInfoState &info) { + info.platform = "macos"; + + info.model = sysctl_string("hw.model"); + if (info.model.empty()) { + info.model = "Mac"; + } + + char hostname[256] = {}; + info.name = (gethostname(hostname, sizeof(hostname) - 1) == 0 && + hostname[0] != '\0') + ? hostname + : info.model; + + const std::string product_version = sysctl_string("kern.osproductversion"); + info.os_version = + product_version.empty() ? "macOS" : "macOS " + product_version; + + info.chip = sysctl_string("machdep.cpu.brand_string"); + if (info.chip.empty()) { + info.chip = "unknown"; + } + + info.total_memory = sysctl_i64("hw.memsize"); + info.available_memory = 0; + + const int64_t ncpu = sysctl_i64("hw.ncpu"); + info.core_count = ncpu > 0 ? static_cast(ncpu) : 1; + const int64_t perf = sysctl_i64("hw.perflevel0.logicalcpu"); + const int64_t eff = sysctl_i64("hw.perflevel1.logicalcpu"); + if (perf > 0) { + info.performance_cores = static_cast(perf); + info.efficiency_cores = eff > 0 ? static_cast(eff) : 0; + } else { + info.performance_cores = info.core_count; + info.efficiency_cores = 0; + } + + struct utsname uts = {}; + info.architecture = (uname(&uts) == 0 && uts.machine[0] != '\0') + ? uts.machine + : "unknown"; + + info.form_factor = + info.model.find("Book") != std::string::npos ? "laptop" : "desktop"; +#if defined(__arm64__) || defined(__aarch64__) + info.gpu_family = "apple"; +#else + info.gpu_family = "unknown"; +#endif +} + +#else // _WIN32 + +void collect_device_info(DeviceInfoState &info) { + info.platform = "windows"; + + char computer_name[MAX_COMPUTERNAME_LENGTH + 1] = {}; + DWORD name_len = sizeof(computer_name); + info.name = GetComputerNameA(computer_name, &name_len) ? computer_name + : "Windows PC"; + info.model = "Windows PC"; + info.os_version = "Windows"; + + char cpu_name[256] = {}; + DWORD cpu_name_size = sizeof(cpu_name); + if (RegGetValueA(HKEY_LOCAL_MACHINE, + "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", + "ProcessorNameString", RRF_RT_REG_SZ, nullptr, cpu_name, + &cpu_name_size) == ERROR_SUCCESS && + cpu_name[0] != '\0') { + info.chip = trim(cpu_name); + } else { + info.chip = "unknown"; + } + + MEMORYSTATUSEX mem = {}; + mem.dwLength = sizeof(mem); + if (GlobalMemoryStatusEx(&mem)) { + info.total_memory = static_cast(mem.ullTotalPhys); + info.available_memory = static_cast(mem.ullAvailPhys); + } + + SYSTEM_INFO sys = {}; + GetNativeSystemInfo(&sys); + info.core_count = sys.dwNumberOfProcessors > 0 + ? static_cast(sys.dwNumberOfProcessors) + : 1; + info.performance_cores = info.core_count; + info.efficiency_cores = 0; + switch (sys.wProcessorArchitecture) { + case PROCESSOR_ARCHITECTURE_AMD64: + info.architecture = "x86_64"; + break; + case PROCESSOR_ARCHITECTURE_ARM64: + info.architecture = "arm64"; + break; + case PROCESSOR_ARCHITECTURE_INTEL: + info.architecture = "x86"; + break; + default: + info.architecture = "unknown"; + break; + } + + SYSTEM_POWER_STATUS power = {}; + if (GetSystemPowerStatus(&power) && power.BatteryFlag != 128 && + power.BatteryFlag != 255) { + info.form_factor = "laptop"; + if (power.BatteryLifePercent <= 100) { + info.battery_level = + static_cast(power.BatteryLifePercent) / 100.0; + if (power.ACLineStatus == 1) { + info.battery_state = power.BatteryLifePercent == 100 ? "full" : "charging"; + } else { + info.battery_state = "unplugged"; + } + } + } else { + info.form_factor = "desktop"; + } + info.gpu_family = "unknown"; +} + +#endif + +void device_get_info(rac_device_registration_info_t *out_info, + void * /*user_data*/) { + if (out_info == nullptr) { + return; + } + auto &info = state(); + info.battery_level = -1.0; + info.battery_state.clear(); + collect_device_info(info); + info.fingerprint = runanywhere::sha256_hex( + info.model + "|" + info.chip + "|" + std::to_string(info.total_memory) + + "|" + std::to_string(info.core_count)); + + *out_info = {}; + out_info->device_id = info.device_id.c_str(); + out_info->device_model = info.model.c_str(); + out_info->device_name = info.name.c_str(); + out_info->platform = info.platform.c_str(); + out_info->os_version = info.os_version.c_str(); + out_info->form_factor = info.form_factor.c_str(); + out_info->architecture = info.architecture.c_str(); + out_info->chip_name = info.chip.c_str(); + out_info->total_memory = info.total_memory; + out_info->available_memory = info.available_memory; + out_info->has_neural_engine = RAC_FALSE; + out_info->neural_engine_cores = 0; + out_info->gpu_family = info.gpu_family.c_str(); + out_info->battery_level = info.battery_level; + out_info->battery_state = + info.battery_state.empty() ? nullptr : info.battery_state.c_str(); + out_info->is_low_power_mode = RAC_FALSE; + out_info->core_count = info.core_count; + out_info->performance_cores = info.performance_cores; + out_info->efficiency_cores = info.efficiency_cores; + out_info->device_fingerprint = info.fingerprint.c_str(); +} + +const char *device_get_id(void * /*user_data*/) { + return state().device_id.c_str(); +} + +rac_bool_t device_is_registered(void * /*user_data*/) { + return state().registered ? RAC_TRUE : RAC_FALSE; +} + +void device_set_registered(rac_bool_t registered, void * /*user_data*/) { + state().registered = registered == RAC_TRUE; +} + +// Same control-plane POST shape as rcli_telemetry_http_callback: commons base +// URL + relative endpoint over the registered desktop HTTP transport, bearer +// token attached when the auth manager holds one. +rac_result_t device_http_post(const char *endpoint, const char *json_body, + rac_bool_t requires_auth, + rac_device_http_response_t *out_response, + void * /*user_data*/) { + auto &info = state(); + info.http_body.clear(); + info.http_error.clear(); + + auto fail = [&](rac_result_t rc, const char *message) { + info.http_error = message; + if (out_response != nullptr) { + out_response->result = rc; + out_response->status_code = 0; + out_response->response_body = nullptr; + out_response->error_message = info.http_error.c_str(); + } + return rc; + }; + + if (endpoint == nullptr || json_body == nullptr) { + return fail(RAC_ERROR_INVALID_ARGUMENT, "invalid registration request"); + } + + const char *base_url = rac_state_get_base_url(); + if (base_url == nullptr || base_url[0] == '\0' || + rac_http_transport_is_registered() != RAC_TRUE) { + return fail(RAC_ERROR_NETWORK_ERROR, + "device registration transport unavailable"); + } + + char url[2048] = {}; + if (rac_build_url(base_url, endpoint, url, sizeof(url)) < 0) { + return fail(RAC_ERROR_NETWORK_ERROR, "device registration URL build failed"); + } + + std::vector headers; + const rac_http_header_kv_t *defaults = nullptr; + size_t default_count = 0; + if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && + defaults != nullptr) { + headers.assign(defaults, defaults + default_count); + } + std::string auth_value; + if (requires_auth == RAC_TRUE) { + const char *token = rac_auth_get_access_token(); + if (token != nullptr && token[0] != '\0') { + auth_value = std::string("Bearer ") + token; + headers.push_back({"Authorization", auth_value.c_str()}); + } + } + + rac_http_client_t *client = nullptr; + if (rac_http_client_create(&client) != RAC_SUCCESS) { + return fail(RAC_ERROR_NETWORK_ERROR, + "device registration client create failed"); + } + + rac_http_request_t request = {}; + request.method = "POST"; + request.url = url; + request.headers = headers.empty() ? nullptr : headers.data(); + request.header_count = headers.size(); + request.body_bytes = reinterpret_cast(json_body); + request.body_len = std::char_traits::length(json_body); + request.timeout_ms = + rac_env_default_http_timeout_ms(rac_state_get_environment()); + request.follow_redirects = RAC_FALSE; + + rac_http_response_t response = {}; + const rac_result_t rc = rac_http_request_send(client, &request, &response); + rac_http_client_destroy(client); + + if (response.body_bytes != nullptr && response.body_len > 0) { + info.http_body.assign(reinterpret_cast(response.body_bytes), + response.body_len); + } + const int32_t status = response.status; + rac_http_response_free(&response); + + const bool ok = rc == RAC_SUCCESS && status >= 200 && status < 300; + if (!ok) { + info.http_error = "device registration POST failed (http " + + std::to_string(status) + ")"; + } + if (out_response != nullptr) { + out_response->result = ok ? RAC_SUCCESS : RAC_ERROR_NETWORK_ERROR; + out_response->status_code = status; + out_response->response_body = + info.http_body.empty() ? nullptr : info.http_body.c_str(); + out_response->error_message = ok ? nullptr : info.http_error.c_str(); + } + return ok ? RAC_SUCCESS : RAC_ERROR_NETWORK_ERROR; +} + +} // namespace + +rac_result_t install_device_callbacks() { + auto &info = state(); + char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; + if (rac_device_get_or_create_persistent_id(device_id, sizeof(device_id)) == + RAC_SUCCESS && + device_id[0] != '\0') { + info.device_id = device_id; + } + + rac_device_callbacks_t callbacks = {}; + callbacks.get_device_info = device_get_info; + callbacks.get_device_id = device_get_id; + callbacks.is_registered = device_is_registered; + callbacks.set_registered = device_set_registered; + callbacks.http_post = device_http_post; + callbacks.user_data = nullptr; + return rac_device_manager_set_callbacks(&callbacks); +} + +} // namespace rcli diff --git a/sdk/runanywhere-cli/src/device_info.h b/sdk/runanywhere-cli/src/device_info.h new file mode 100644 index 0000000000..d18262b86e --- /dev/null +++ b/sdk/runanywhere-cli/src/device_info.h @@ -0,0 +1,15 @@ +#ifndef RCLI_DEVICE_INFO_H +#define RCLI_DEVICE_INFO_H + +#include "rac/core/rac_types.h" + +namespace rcli { + +// Installs the desktop device-registration callbacks on the commons device +// manager. Must run before SDK phase 2 so registration carries real hardware +// info instead of being skipped for missing callbacks. +rac_result_t install_device_callbacks(); + +} // namespace rcli + +#endif // RCLI_DEVICE_INFO_H diff --git a/sdk/runanywhere-commons/CMakeLists.txt b/sdk/runanywhere-commons/CMakeLists.txt index 8571dcb6da..a3c7a191e7 100644 --- a/sdk/runanywhere-commons/CMakeLists.txt +++ b/sdk/runanywhere-commons/CMakeLists.txt @@ -839,6 +839,7 @@ set(RAC_INFRASTRUCTURE_SOURCES src/infrastructure/telemetry/telemetry_json.cpp src/infrastructure/telemetry/telemetry_manager.cpp src/infrastructure/device/rac_device_manager.cpp + src/infrastructure/device/cpu_state.cpp # Persistent device-id resolution chain # (Keychain -> identifierForVendor -> generate UUID) collapsed from each # platform SDK into a single C entry point. diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h index 4ac041512b..502b29ac8c 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h @@ -94,10 +94,10 @@ typedef struct rac_telemetry_payload { int32_t segment_index; // Hybrid STT router attribution (populated only on the hybrid transcribe // path; null on plain single-backend STT). - const char* routed_backend; // backend/model that actually served the request - rac_bool_t was_fallback; // secondary served after the primary failed - rac_bool_t has_was_fallback; // whether was_fallback is meaningful - int32_t attempt_count; // 1 = primary only, 2 = primary then fallback + const char* routed_backend; // backend/model that actually served the request + rac_bool_t was_fallback; // secondary served after the primary failed + rac_bool_t has_was_fallback; // whether was_fallback is meaningful + int32_t attempt_count; // 1 = primary only, 2 = primary then fallback // TTS-specific fields int32_t character_count; @@ -132,9 +132,9 @@ typedef struct rac_telemetry_payload { // RAG-specific extras (via properties carrier; retrieved_docs_count above) int32_t top_k; double retrieval_time_ms; - int32_t query_token_count; // estimated tokens in the query (via properties carrier) - int32_t context_tokens; // estimated tokens in the retrieved context (via carrier) - const char* embedding_model; // RAG embedding model (string → dup'd/freed) + int32_t query_token_count; // estimated tokens in the query (via properties carrier) + int32_t context_tokens; // estimated tokens in the retrieved context (via carrier) + const char* embedding_model; // RAG embedding model (string → dup'd/freed) rac_bool_t reranker_used; // LLM-pointwise rerank enabled for the query rac_bool_t has_reranker_used; // whether reranker_used is set @@ -171,9 +171,9 @@ typedef struct rac_telemetry_payload { // Voice-agent per-turn fields (via properties carrier; voice_* timing above) int32_t transcript_chars; int32_t response_chars; - int32_t turn_index; // 0-based turn number within the agent session - rac_bool_t has_turn_index; // whether turn_index is set (0 is valid) - rac_bool_t voice_interrupted; // turn ended via caller cancel / barge-out + int32_t turn_index; // 0-based turn number within the agent session + rac_bool_t has_turn_index; // whether turn_index is set (0 is valid) + rac_bool_t voice_interrupted; // turn ended via caller cancel / barge-out rac_bool_t has_voice_interrupted; // SDK lifecycle fields @@ -185,6 +185,18 @@ typedef struct rac_telemetry_payload { // Network fields rac_bool_t is_online; rac_bool_t has_is_online; + + // Live device state + SDK origin (stamped by the telemetry manager at + // track time — callers never set these; see telemetry_manager.cpp) + const char* sdk_binding; // "swift", "kotlin", "flutter", "react-native", "web", "cli" + double battery_level; // 0.0-1.0 sampled at event time, negative if unavailable + const char* battery_state; // "charging", "full", "unplugged", NULL if unavailable + rac_bool_t is_low_power_mode; // Low power mode at event time + rac_bool_t has_is_low_power_mode; + int64_t total_memory; // Total RAM in bytes, 0 if unknown + int64_t available_memory; // Available RAM in bytes at event time, 0 if unknown + double cpu_usage_percent; // 0-100 since previous event, negative if unavailable + int32_t online_core_count; // CPU cores online at event time, 0 if unknown } rac_telemetry_payload_t; /** diff --git a/sdk/runanywhere-commons/src/desktop/desktop_adapter.cpp b/sdk/runanywhere-commons/src/desktop/desktop_adapter.cpp index 198dae4550..e411bbd22c 100644 --- a/sdk/runanywhere-commons/src/desktop/desktop_adapter.cpp +++ b/sdk/runanywhere-commons/src/desktop/desktop_adapter.cpp @@ -17,6 +17,7 @@ * UUID through secure storage instead. */ +#include "../infrastructure/device/rac_device_live_state_internal.h" #include #include #include @@ -454,6 +455,9 @@ extern "C" { rac_result_t rac_desktop_adapter_init(const rac_desktop_adapter_config_t* config, rac_platform_adapter_t* out_adapter) { + // Desktop callbacks are plain C — live telemetry sampling is thread-safe. + rac_telemetry_enable_live_platform_sampling(); + if (!out_adapter) { return RAC_ERROR_INVALID_ARGUMENT; } diff --git a/sdk/runanywhere-commons/src/infrastructure/device/cpu_state.cpp b/sdk/runanywhere-commons/src/infrastructure/device/cpu_state.cpp new file mode 100644 index 0000000000..a0ce65acc7 --- /dev/null +++ b/sdk/runanywhere-commons/src/infrastructure/device/cpu_state.cpp @@ -0,0 +1,211 @@ +/** + * @file cpu_state.cpp + * @brief In-process CPU state sampling for per-event telemetry. + * + * Reads OS accounting directly (no platform-adapter callback needed): + * - Linux/Android: /proc/stat aggregate; falls back to /proc/self/stat + * (process CPU normalized to total capacity) where SELinux hides /proc/stat. + * - Apple: host_statistics(HOST_CPU_LOAD_INFO). + * - Windows: GetSystemTimes(). + * - Others (WASM): unavailable, reports -1. + * + * Usage percent is the busy/total tick delta since the previous call, so the + * telemetry manager gets "average CPU since the last event" — the useful + * number for benchmark runs. First call establishes the baseline. + */ + +#include "rac_device_live_state_internal.h" + +#include + +#if defined(_WIN32) +#include +#elif defined(__APPLE__) +#include + +#include +#include +#include +#else +#include + +#include +#include +#include +#include +#endif + +namespace { + +std::mutex g_cpu_mutex; + +#if defined(_WIN32) + +uint64_t filetime_u64(const FILETIME& ft) { + return (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +} + +bool read_ticks(uint64_t* busy, uint64_t* total) { + FILETIME idle_ft, kernel_ft, user_ft; + if (!GetSystemTimes(&idle_ft, &kernel_ft, &user_ft)) { + return false; + } + const uint64_t idle = filetime_u64(idle_ft); + const uint64_t kernel = filetime_u64(kernel_ft); // includes idle + const uint64_t user = filetime_u64(user_ft); + *total = kernel + user; + *busy = *total - idle; + return true; +} + +#elif defined(__APPLE__) + +bool read_ticks(uint64_t* busy, uint64_t* total) { + host_cpu_load_info_data_t info; + mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT; + if (host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, reinterpret_cast(&info), + &count) != KERN_SUCCESS) { + return false; + } + const uint64_t user = info.cpu_ticks[CPU_STATE_USER]; + const uint64_t system = info.cpu_ticks[CPU_STATE_SYSTEM]; + const uint64_t nice = info.cpu_ticks[CPU_STATE_NICE]; + const uint64_t idle = info.cpu_ticks[CPU_STATE_IDLE]; + *busy = user + system + nice; + *total = *busy + idle; + return true; +} + +#else + +// Aggregate system ticks from /proc/stat. Unreadable on Android 8+ app +// processes (SELinux) — read_process_ticks covers that case. +bool read_ticks(uint64_t* busy, uint64_t* total) { + FILE* f = fopen("/proc/stat", "re"); + if (!f) { + return false; + } + uint64_t user = 0, nice = 0, system = 0, idle = 0, iowait = 0, irq = 0, softirq = 0, steal = 0; + const int matched = fscanf(f, + "cpu %" SCNu64 " %" SCNu64 " %" SCNu64 " %" SCNu64 " %" SCNu64 + " %" SCNu64 " %" SCNu64 " %" SCNu64, + &user, &nice, &system, &idle, &iowait, &irq, &softirq, &steal); + fclose(f); + if (matched < 4) { + return false; + } + *busy = user + nice + system + irq + softirq + steal; + *total = *busy + idle + iowait; + return true; +} + +// Process CPU ticks + wall clock, for the /proc/stat-restricted fallback. +// "total" is wall time scaled to all cores so the percentage stays 0-100 +// relative to full device capacity. +bool read_process_ticks(uint64_t* busy, uint64_t* total) { + FILE* f = fopen("/proc/self/stat", "re"); + if (!f) { + return false; + } + // Fields 14 (utime) and 15 (stime); field 2 (comm) may contain spaces, so + // skip past the closing paren first. + char buf[512]; + const size_t n = fread(buf, 1, sizeof(buf) - 1, f); + fclose(f); + if (n == 0) { + return false; + } + buf[n] = '\0'; + const char* p = strrchr(buf, ')'); + if (!p) { + return false; + } + p += 1; + uint64_t utime = 0, stime = 0; + // After comm: state + 10 fields precede utime. + char state = 0; + unsigned long long skip[10] = {}; + if (sscanf(p, " %c %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %" SCNu64 " %" SCNu64, + &state, &skip[0], &skip[1], &skip[2], &skip[3], &skip[4], &skip[5], &skip[6], + &skip[7], &skip[8], &skip[9], &utime, &stime) < 13) { + return false; + } + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return false; + } + const long ticks_per_sec = sysconf(_SC_CLK_TCK); + const long ncpu = sysconf(_SC_NPROCESSORS_ONLN); + if (ticks_per_sec <= 0 || ncpu <= 0) { + return false; + } + *busy = utime + stime; + const uint64_t wall_ticks = (static_cast(ts.tv_sec) * ticks_per_sec) + + (static_cast(ts.tv_nsec) * ticks_per_sec / 1000000000ULL); + *total = wall_ticks * static_cast(ncpu); + return true; +} + +#endif + +} // namespace + +extern "C" { + +double rac_cpu_sample_usage_percent(void) { + std::lock_guard lock(g_cpu_mutex); + + static uint64_t prev_busy = 0; + static uint64_t prev_total = 0; + static bool has_baseline = false; + + uint64_t busy = 0, total = 0; + bool ok = read_ticks(&busy, &total); +#if !defined(_WIN32) && !defined(__APPLE__) + static bool use_process_fallback = false; + if (!ok || use_process_fallback) { + use_process_fallback = true; + ok = read_process_ticks(&busy, &total); + } +#endif + if (!ok) { + return -1.0; + } + + if (!has_baseline) { + prev_busy = busy; + prev_total = total; + has_baseline = true; + return -1.0; + } + + const uint64_t busy_delta = busy >= prev_busy ? busy - prev_busy : 0; + const uint64_t total_delta = total >= prev_total ? total - prev_total : 0; + prev_busy = busy; + prev_total = total; + + if (total_delta == 0) { + return -1.0; + } + double pct = 100.0 * static_cast(busy_delta) / static_cast(total_delta); + if (pct < 0.0) { + pct = 0.0; + } + if (pct > 100.0) { + pct = 100.0; + } + return pct; +} + +int32_t rac_cpu_online_core_count(void) { +#if defined(_WIN32) + SYSTEM_INFO info; + GetSystemInfo(&info); + return static_cast(info.dwNumberOfProcessors); +#else + const long n = sysconf(_SC_NPROCESSORS_ONLN); + return n > 0 ? static_cast(n) : 0; +#endif +} + +} // extern "C" diff --git a/sdk/runanywhere-commons/src/infrastructure/device/rac_device_live_state_internal.h b/sdk/runanywhere-commons/src/infrastructure/device/rac_device_live_state_internal.h new file mode 100644 index 0000000000..9d23b8906e --- /dev/null +++ b/sdk/runanywhere-commons/src/infrastructure/device/rac_device_live_state_internal.h @@ -0,0 +1,84 @@ +/** + * @file rac_device_live_state_internal.h + * @brief Internal live device-state sampling for per-event telemetry. + * + * Not part of the public C ABI. The telemetry manager stamps every tracked + * event with a live device snapshot (battery, RAM, CPU) via these helpers. + */ + +#ifndef RAC_DEVICE_LIVE_STATE_INTERNAL_H +#define RAC_DEVICE_LIVE_STATE_INTERNAL_H + +#include + +#include "rac/core/rac_error.h" +#include "rac/core/rac_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rac_device_live_state { + double battery_level; /* 0.0-1.0, negative if unavailable */ + char battery_state[16]; /* "charging"/"full"/"unplugged", "" if unavailable */ + rac_bool_t is_low_power_mode; + rac_bool_t has_low_power_mode; + int64_t total_memory; /* bytes, 0 if unknown */ + int64_t available_memory; /* bytes, 0 if unknown */ +} rac_device_live_state_t; + +/** + * Sample live device state via the registered device-manager callbacks. + * Returns RAC_ERROR_NOT_INITIALIZED when no callbacks are set (e.g. CLI + * before wiring, early init) — caller keeps unknown sentinels. + */ +rac_result_t rac_device_manager_sample_live_state(rac_device_live_state_t* out); + +/** + * System CPU usage percent (0-100) averaged since the previous call. + * First call establishes the baseline and returns -1. Returns -1 when the + * platform exposes no CPU accounting (e.g. WASM). Thread-safe. + */ +double rac_cpu_sample_usage_percent(void); + +/** CPU cores currently online; 0 if unknown. */ +int32_t rac_cpu_online_core_count(void); + +/** + * Server-driven registration heal. The authenticate response carries + * device_registered=false when the backend just minted (or still holds) an + * "Unknown"/"SDK Device" placeholder row for this device — the client's + * persisted is_registered flag is stale in that case, and production mode + * would otherwise skip registration forever. Calling this forces the next + * rac_device_manager_register_if_needed() to register regardless of the + * platform-persisted flag; a successful registration clears it. + */ +void rac_device_manager_notify_server_unregistered(void); + +/** + * Opt-in for live platform sampling (battery/RAM via platform callbacks) on + * telemetry events. Only bridges whose callbacks are thread-safe from any + * thread may enable this (JNI attaches threads; the desktop adapter is plain + * C). Dart FFI callbacks are isolate-bound and MUST NOT be invoked from + * inference/telemetry threads — Flutter stays disabled until its bridge + * pushes state instead. In-process fields (CPU, core count, sdk_binding) + * are always stamped regardless. + */ +void rac_telemetry_enable_live_platform_sampling(void); + +/** + * Push model for isolate-bound bridges (Dart FFI): the platform pushes fresh + * battery/RAM values from its own thread; telemetry stamping reads only this + * cache. battery_level negative = unknown; battery_state NULL/"" = unknown; + * memory 0 = unknown. Exported with default visibility on Android/desktop so + * Dart can bind it by name. + */ +void rac_telemetry_push_live_device_state(double battery_level, const char* battery_state, + rac_bool_t is_low_power_mode, int64_t total_memory, + int64_t available_memory); + +#ifdef __cplusplus +} +#endif + +#endif /* RAC_DEVICE_LIVE_STATE_INTERNAL_H */ diff --git a/sdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cpp b/sdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cpp index 3a2e923d61..cc6517a797 100644 --- a/sdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cpp @@ -8,6 +8,8 @@ #include "rac/infrastructure/device/rac_device_manager.h" +#include "rac_device_live_state_internal.h" + #include #include #include @@ -29,6 +31,10 @@ namespace { struct DeviceManagerState { rac_device_callbacks_t callbacks = {}; bool callbacks_set = false; + // Set when the authenticate response reported device_registered=false — + // the server holds only a placeholder row, so the platform-persisted + // is_registered flag is stale and must not skip registration. + bool server_unregistered = false; std::mutex mutex; }; @@ -89,6 +95,51 @@ void rac_device_manager_clear_callbacks(void) { RAC_LOG_INFO(LOG_CAT, "Device manager callbacks cleared"); } +void rac_device_manager_notify_server_unregistered(void) { + auto& state = get_state(); + std::lock_guard lock(state.mutex); + state.server_unregistered = true; + RAC_LOG_INFO(LOG_CAT, + "Server reports device not registered (placeholder row) — next " + "registration attempt will run regardless of the persisted flag"); +} + +rac_result_t rac_device_manager_sample_live_state(rac_device_live_state_t* out) { + if (!out) { + return RAC_ERROR_INVALID_ARGUMENT; + } + *out = {}; + out->battery_level = -1.0; + + auto& state = get_state(); + // try_lock: never let event tracking block behind an in-flight + // registration HTTP round-trip; the caller keeps unknown sentinels. + std::unique_lock lock(state.mutex, std::try_to_lock); + if (!lock.owns_lock()) { + return RAC_ERROR_NOT_INITIALIZED; + } + if (!state.callbacks_set || !state.callbacks.get_device_info) { + return RAC_ERROR_NOT_INITIALIZED; + } + + // Platform fills a stack struct; string fields point at platform-owned + // storage (same contract as registration) — copy what we keep before + // releasing the lock. + rac_device_registration_info_t info = {}; + info.battery_level = -1.0; + state.callbacks.get_device_info(&info, state.callbacks.user_data); + + out->battery_level = info.battery_level; + if (info.battery_state && info.battery_state[0] != '\0') { + strncpy(out->battery_state, info.battery_state, sizeof(out->battery_state) - 1); + } + out->is_low_power_mode = info.is_low_power_mode; + out->has_low_power_mode = RAC_TRUE; + out->total_memory = info.total_memory; + out->available_memory = info.available_memory; + return RAC_SUCCESS; +} + rac_result_t rac_device_manager_register_if_needed(rac_environment_t env, const char* build_token) { auto& state = get_state(); std::lock_guard lock(state.mutex); @@ -104,8 +155,13 @@ rac_result_t rac_device_manager_register_if_needed(rac_environment_t env, const // Step 1: Check if already registered // Production behavior: Skip if already registered (performance, network efficiency) // Development behavior: Always update via UPSERT (track active devices, update last_seen_at) + // Server heal: the authenticate response can report device_registered=false + // (backend holds only a placeholder row), which overrides the stale + // platform-persisted flag — otherwise a server-side device reset leaves an + // "Unknown"/"SDK Device" row that production mode never upgrades. const bool was_registered = - state.callbacks.is_registered(state.callbacks.user_data) == RAC_TRUE; + state.callbacks.is_registered(state.callbacks.user_data) == RAC_TRUE && + !state.server_unregistered; if (was_registered && env != RAC_ENV_DEVELOPMENT) { RAC_LOG_DEBUG(LOG_CAT, "Device already registered, skipping (production mode)"); // Skip the network round-trip, but still emit the device.registered @@ -177,8 +233,9 @@ rac_result_t rac_device_manager_register_if_needed(rac_environment_t env, const RAC_LOG_DEBUG(LOG_CAT, "Registration endpoint: %s", endpoint); RAC_LOG_DEBUG(LOG_CAT, "Registration payload prepared (%zu bytes)", json_len); - // Step 7: Determine if auth is required (staging/production require auth) - rac_bool_t requires_auth = (env != RAC_ENV_DEVELOPMENT) ? RAC_TRUE : RAC_FALSE; + // Step 7: The register endpoint always requires the SDK bearer token. + rac_bool_t requires_auth = RAC_TRUE; + (void)env; // Step 8: Make HTTP request via callback rac_device_http_response_t response = {}; @@ -210,8 +267,9 @@ rac_result_t rac_device_manager_register_if_needed(rac_environment_t env, const return response_result; } - // Step 10: Mark as registered + // Step 10: Mark as registered (server placeholder, if any, is now upgraded) state.callbacks.set_registered(RAC_TRUE, state.callbacks.user_data); + state.server_unregistered = false; // Step 11: Emit success event emit_device_registered(device_id); diff --git a/sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp b/sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp index 9f80cc922a..90497beb1d 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp @@ -16,6 +16,11 @@ #include "rac/infrastructure/network/rac_api_types.h" #include "rac/infrastructure/network/rac_auth_manager.h" +#include "rac/core/rac_sdk_state.h" +#include "rac/infrastructure/device/rac_device_manager.h" + +#include "../device/rac_device_live_state_internal.h" + // ============================================================================= // Global State // ============================================================================= @@ -342,6 +347,18 @@ static int handle_auth_response(const char* json, bool refresh) { return -1; } + // Registration heal: the authenticate response reports + // device_registered=false when the backend only holds a placeholder row + // for this device. Flag the device manager so the phase-2 registration + // runs even when the platform-persisted is_registered flag says otherwise. + // Absent field (older backends) means no override. + const bool server_unregistered = + !refresh && (strstr(json, "\"device_registered\":false") != nullptr || + strstr(json, "\"device_registered\": false") != nullptr); + if (server_unregistered) { + rac_device_manager_notify_server_unregistered(); + } + int result; { std::lock_guard lock(g_auth_mutex); @@ -370,6 +387,12 @@ static int handle_auth_response(const char* json, bool refresh) { // A token is now available — drain telemetry batches deferred by the // pre-auth flush gate (see rac_telemetry_manager_flush). rac_events_flush_telemetry_sink(); + // The freshly minted token is now available: run the forced + // registration immediately so the server's placeholder row upgrades + // this session instead of on the next launch. + if (server_unregistered) { + (void)rac_device_manager_register_if_needed(rac_state_get_environment(), nullptr); + } } else { if (result == RAC_ERROR_SECURE_STORAGE_FAILED) { publish_auth_failure_event("Failed to persist authentication state", refresh, diff --git a/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp b/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp index 1d314c3a56..44c3eedcc2 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp @@ -13,10 +13,11 @@ const char* rac_endpoint_device_registration(rac_environment_t env) { switch (env) { case RAC_ENV_DEVELOPMENT: - return RAC_ENDPOINT_DEV_DEVICE_REGISTER; case RAC_ENV_STAGING: case RAC_ENV_PRODUCTION: default: + // Every environment registers against the FastAPI backend; the + // Supabase-direct dev path is retired. return RAC_ENDPOINT_DEVICE_REGISTER; } } diff --git a/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp b/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp index 557d4b6515..3cd10b7038 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp @@ -66,7 +66,8 @@ bool rac_env_requires_auth(rac_environment_t env) { bool rac_env_auth_expected(rac_environment_t env, const char* api_key) { if (!rac_env_requires_auth(env)) { - return false; + // Development authenticates when the caller supplied an explicit key. + return api_key != nullptr && api_key[0] != '\0'; } // Staging accepts keyless clients — requests go out unauthenticated and // the backend attributes them to the PUBLIC org. Production stays strict. diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp index 79952e58f8..eb15912a49 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp @@ -284,6 +284,19 @@ rac_result_t rac_telemetry_manager_payload_to_json(const rac_telemetry_payload_t json.add_string("error_code", payload->error_code); json.add_bool("is_streaming", payload->is_streaming, payload->has_is_streaming); + // ---- SDK origin + live device state (stamped by the manager) ---------- + json.add_string("sdk_binding", payload->sdk_binding); + json.add_double_or_null("battery_level", payload->battery_level, payload->battery_level >= 0); + json.add_string_or_null("battery_state", payload->battery_state); + json.add_bool("is_low_power_mode", payload->is_low_power_mode, payload->has_is_low_power_mode); + json.add_int_or_null("total_memory", payload->total_memory, payload->total_memory > 0); + json.add_int_or_null("available_memory", payload->available_memory, + payload->available_memory > 0); + json.add_double_or_null("cpu_usage_percent", payload->cpu_usage_percent, + payload->cpu_usage_percent >= 0); + json.add_int_or_null("online_core_count", payload->online_core_count, + payload->online_core_count > 0); + // ---- Modality-specific fields ------------------------------------------ const char* modality = payload->modality ? payload->modality : "system"; if (strcmp(modality, "llm") == 0) { @@ -483,56 +496,13 @@ rac_result_t rac_device_registration_to_json(const rac_device_registration_reque JsonBuilder json; json.start_object(); - // For development mode (Supabase), flatten the structure to match Supabase schema - // For production/staging, use nested device_info structure - if (env == RAC_ENV_DEVELOPMENT) { - // Flattened structure for Supabase (matches Kotlin SDK DevDeviceRegistrationRequest) - const rac_device_registration_info_t* info = &request->device_info; - - // Required fields (matching Supabase schema) - if (info->device_id) { - json.add_string("device_id", info->device_id); - } - if (info->platform) { - json.add_string("platform", info->platform); - } - if (info->os_version) { - json.add_string("os_version", info->os_version); - } - if (info->device_model) { - json.add_string("device_model", info->device_model); - } - if (request->sdk_version) { - json.add_string("sdk_version", request->sdk_version); - } - if (has_client_info(request->client_info)) { - add_client_info_fields(json, request->client_info); - } - - // Optional fields - if (request->build_token) { - json.add_string("build_token", request->build_token); - } - if (info->total_memory > 0) { - json.add_int("total_memory", info->total_memory); - } - if (info->architecture) { - json.add_string("architecture", info->architecture); - } - if (info->chip_name) { - json.add_string("chip_name", info->chip_name); - } - if (info->form_factor) { - json.add_string("form_factor", info->form_factor); - } - // has_neural_engine is always set (rac_bool_t), so we can always include it - json.add_bool("has_neural_engine", info->has_neural_engine, RAC_TRUE); - // Add last_seen_at timestamp for UPSERT to update existing records - if (request->last_seen_at_ms > 0) { - json.add_timestamp("last_seen_at", request->last_seen_at_ms); - } - } else { - // Nested structure for production/staging + // Nested device_info structure for every environment. The backend route + // (POST /devices/register, DeviceRegistrationRequest) requires it; the old + // development-only flat shape (a direct-to-Supabase relic) 422'd against + // the FastAPI backend, so dev devices never upgraded their auth-time + // "Unknown"/"SDK Device" placeholder row. + (void)env; + { // Matches backend schemas/device.py DeviceInfo schema const rac_device_registration_info_t* info = &request->device_info; diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp index 5213ccd873..fe0c681cf9 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp @@ -5,6 +5,7 @@ * Handles event queuing, batching by modality, and HTTP callbacks. */ +#include #include #include #include @@ -16,7 +17,9 @@ #include #include +#include "../device/rac_device_live_state_internal.h" #include "rac/core/rac_logger.h" +#include "rac/core/rac_platform_adapter.h" #include "rac/core/rac_sdk_state.h" #include "rac/infrastructure/network/rac_auth_manager.h" #include "rac/infrastructure/network/rac_endpoints.h" @@ -74,6 +77,34 @@ struct rac_telemetry_manager { // platform that never drains (Flutter isolate gone) grew it without limit. static constexpr size_t MAX_HTTP_QUEUE_SIZE = 64; int64_t last_flush_time_ms = 0; // Track last flush time for timeout + + // Live device-state snapshot cached across events. Sampling goes through + // the device manager's platform callback (JNI/Swift/FFI), so a short TTL + // bounds the cost without staling benchmark-relevant readings. + static constexpr int64_t LIVE_STATE_TTL_MS = 5000; + std::mutex live_state_mutex; + rac_device_live_state_t live_state_cache = {}; + int64_t live_state_sampled_ms = 0; + bool live_state_valid = false; + + // Bounded HTTP retry. Sent batches sit in `inflight` until the platform + // reports the outcome via rac_telemetry_manager_http_complete; failures + // move to `retry_queue` with exponential backoff and re-send on a later + // flush. Safe at-least-once: the backend dedupes on the per-event unique + // id, so a batch retried after an ambiguous failure never double-counts. + struct PendingBatch { + std::string endpoint; + std::string json; + int attempts = 0; + int64_t next_attempt_ms = 0; + }; + std::deque inflight; + std::deque retry_queue; + std::mutex retry_mutex; + static constexpr int MAX_SEND_ATTEMPTS = 3; + static constexpr size_t MAX_RETRY_QUEUE = 16; + static constexpr size_t MAX_INFLIGHT = 16; + static constexpr int64_t RETRY_BASE_DELAY_MS = 5000; }; // ============================================================================= @@ -145,6 +176,8 @@ void free_payload_strings(rac_telemetry_payload_t& event) { free((void*)event.scheduler); free((void*)event.output_format); free((void*)event.routed_backend); + free((void*)event.sdk_binding); + free((void*)event.battery_state); } #if defined(RAC_HAVE_PROTOBUF) @@ -351,6 +384,104 @@ rac_result_t rac_telemetry_manager_poll_http_request(rac_telemetry_manager_t* ma // EVENT TRACKING // ============================================================================= +std::atomic g_live_platform_sampling{false}; + +extern "C" void rac_telemetry_enable_live_platform_sampling(void) { + g_live_platform_sampling.store(true, std::memory_order_relaxed); +} + +// Pushed live state for bridges whose callbacks are not thread-safe from +// telemetry threads (Dart FFI is isolate-bound): the platform pushes fresh +// values from its own thread and stamping only ever reads this cache. +namespace { +std::mutex g_pushed_state_mutex; +rac_device_live_state_t g_pushed_state = {}; +bool g_pushed_state_valid = false; +} // namespace + +extern "C" void rac_telemetry_push_live_device_state(double battery_level, + const char* battery_state, + rac_bool_t is_low_power_mode, + int64_t total_memory, + int64_t available_memory) { + std::lock_guard lock(g_pushed_state_mutex); + g_pushed_state = {}; + g_pushed_state.battery_level = battery_level; + if (battery_state != nullptr && battery_state[0] != '\0') { + strncpy(g_pushed_state.battery_state, battery_state, + sizeof(g_pushed_state.battery_state) - 1); + } + g_pushed_state.is_low_power_mode = is_low_power_mode; + g_pushed_state.has_low_power_mode = RAC_TRUE; + g_pushed_state.total_memory = total_memory; + g_pushed_state.available_memory = available_memory; + g_pushed_state_valid = true; +} + +namespace { + +// Stamp SDK origin + a live device snapshot onto the queued copy. Battery and +// registration-derived memory come from the device manager callback behind a +// short TTL cache; RAM is refreshed from the platform adapter when the +// optional get_memory_info slot exists; CPU state is read in-process. Every +// source degrades to unknown sentinels — tracking never fails on this path. +void stamp_live_device_state(rac_telemetry_manager_t* manager, rac_telemetry_payload_t& copy) { + const rac_client_info_t* client_info = rac_sdk_get_client_info(); + if (client_info && client_info->sdk_binding && client_info->sdk_binding[0] != '\0') { + copy.sdk_binding = dup_string(client_info->sdk_binding); + } + + rac_device_live_state_t state = {}; + bool have_state = false; + { + std::lock_guard lock(g_pushed_state_mutex); + if (g_pushed_state_valid) { + state = g_pushed_state; + have_state = true; + } + } + if (!have_state && g_live_platform_sampling.load(std::memory_order_relaxed)) { + std::lock_guard lock(manager->live_state_mutex); + const int64_t now = get_current_timestamp_ms(); + if (manager->live_state_valid && + (now - manager->live_state_sampled_ms) < rac_telemetry_manager::LIVE_STATE_TTL_MS) { + state = manager->live_state_cache; + have_state = true; + } else if (rac_device_manager_sample_live_state(&state) == RAC_SUCCESS) { + manager->live_state_cache = state; + manager->live_state_sampled_ms = now; + manager->live_state_valid = true; + have_state = true; + } + } + if (have_state) { + copy.battery_level = state.battery_level; + if (state.battery_state[0] != '\0') { + copy.battery_state = dup_string(state.battery_state); + } + copy.is_low_power_mode = state.is_low_power_mode; + copy.has_is_low_power_mode = state.has_low_power_mode; + copy.total_memory = state.total_memory; + copy.available_memory = state.available_memory; + } + + const rac_platform_adapter_t* adapter = rac_get_platform_adapter(); + if (g_live_platform_sampling.load(std::memory_order_relaxed) && adapter && + adapter->get_memory_info) { + rac_memory_info_t mem = {}; + if (adapter->get_memory_info(&mem, adapter->user_data) == RAC_SUCCESS && + mem.total_bytes > 0) { + copy.total_memory = static_cast(mem.total_bytes); + copy.available_memory = static_cast(mem.available_bytes); + } + } + + copy.cpu_usage_percent = rac_cpu_sample_usage_percent(); + copy.online_core_count = rac_cpu_online_core_count(); +} + +} // namespace + rac_result_t rac_telemetry_manager_track(rac_telemetry_manager_t* manager, const rac_telemetry_payload_t* payload) { if (!manager || !payload) { @@ -359,6 +490,7 @@ rac_result_t rac_telemetry_manager_track(rac_telemetry_manager_t* manager, // Deep copy payload for queue rac_telemetry_payload_t copy = *payload; + stamp_live_device_state(manager, copy); copy.id = dup_string(payload->id); copy.event_type = dup_string(payload->event_type); copy.modality = dup_string(payload->modality); @@ -925,6 +1057,7 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.has_processing_time_ms = RAC_TRUE; payload.generation_time_ms = dur; payload.tokens_per_second = g.tokens_per_second(); + payload.prompt_eval_time_ms = static_cast(g.prompt_eval_time_ms()); payload.time_to_first_token_ms = g.time_to_first_token_ms() != 0 ? static_cast(g.time_to_first_token_ms()) : static_cast(g.first_token_latency_ms()); @@ -1196,6 +1329,10 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, if (vet_it != ev.properties().end()) { payload.vision_encode_time_ms = std::atof(vet_it->second.c_str()); } + auto pe_it = ev.properties().find("prompt_eval_time_ms"); + if (pe_it != ev.properties().end()) { + payload.prompt_eval_time_ms = std::atof(pe_it->second.c_str()); + } auto ir_it = ev.properties().find("image_resolution"); if (ir_it != ev.properties().end() && !ir_it->second.empty()) { payload.image_resolution = ir_it->second.c_str(); @@ -1224,6 +1361,16 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.reranker_used = rr_it->second == "1" ? RAC_TRUE : RAC_FALSE; payload.has_reranker_used = RAC_TRUE; } + auto qtc_it = ev.properties().find("query_token_count"); + if (qtc_it != ev.properties().end()) { + payload.query_token_count = + static_cast(std::atoi(qtc_it->second.c_str())); + } + auto ctx_it = ev.properties().find("context_tokens"); + if (ctx_it != ev.properties().end()) { + payload.context_tokens = + static_cast(std::atoi(ctx_it->second.c_str())); + } break; } case runanywhere::v1::SDK_COMPONENT_EMBEDDINGS: { @@ -1237,6 +1384,15 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.embedding_dimension = static_cast(std::atoi(dim_it->second.c_str())); } + auto etot_it = ev.properties().find("total_tokens"); + if (etot_it != ev.properties().end()) { + payload.total_tokens = + static_cast(std::atoi(etot_it->second.c_str())); + } + auto bs_it = ev.properties().find("batch_size"); + if (bs_it != ev.properties().end()) { + payload.batch_size = static_cast(std::atoi(bs_it->second.c_str())); + } break; } case runanywhere::v1::SDK_COMPONENT_DIFFUSION: { @@ -1391,8 +1547,9 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, payload.has_success = RAC_TRUE; } else if (ends_with(".completed") || ends_with(".loaded") || ends_with(".deleted") || ends_with(".cleared") || ends_with(".cleaned") || ends_with(".registered") || - ends_with(".stopped") || ends_with(".succeeded") || ends_with(".authenticated") || - ends_with(".refreshed") || ends_with(".token_refreshed")) { + ends_with(".stopped") || ends_with(".succeeded") || + ends_with(".authenticated") || ends_with(".refreshed") || + ends_with(".token_refreshed")) { // ".succeeded"/".authenticated"/".refreshed"/".token_refreshed" cover the // auth lifecycle (auth.succeeded, auth.token_refreshed) whose success flag // was otherwise left null on the system row. @@ -1425,6 +1582,46 @@ rac_result_t rac_telemetry_manager_track_proto(rac_telemetry_manager_t* manager, // FLUSH // ============================================================================= +namespace { + +// Deliver one serialized batch and record it in the in-flight window so +// rac_telemetry_manager_http_complete can retry it on failure. Delivery is +// FIFO per manager (URLSession/OkHttp callbacks preserve submission order for +// these sequential posts), so completions are matched front-of-queue; a +// mismatch only re-sends a real batch, which the backend dedupes by event id. +void send_batch_json(rac_telemetry_manager_t* manager, const std::string& endpoint, + const char* json, size_t json_len, int attempts) { + { + std::lock_guard lock(manager->retry_mutex); + while (manager->inflight.size() >= rac_telemetry_manager::MAX_INFLIGHT) { + // Platform never reported these completions — age them out. + manager->inflight.pop_front(); + } + manager->inflight.push_back({endpoint, std::string(json, json_len), attempts, 0}); + } + + if (manager->http_wakeup) { + // Isolate-safe path: enqueue an owned copy and signal the platform to + // drain it from its own thread/isolate (see poll_http_request). Used + // by Flutter, whose Dart FFI data callbacks are isolate-bound. + { + std::lock_guard lock(manager->http_queue_mutex); + while (manager->http_queue.size() >= rac_telemetry_manager::MAX_HTTP_QUEUE_SIZE) { + RAC_LOG_WARNING("Telemetry", + "HTTP queue full (%zu) — dropping oldest pending batch", + manager->http_queue.size()); + manager->http_queue.pop_front(); + } + manager->http_queue.push_back({endpoint, std::string(json, json_len), true}); + } + manager->http_wakeup(manager->http_wakeup_user_data); + } else if (manager->http_callback) { + manager->http_callback(manager->http_user_data, endpoint.c_str(), json, json_len, RAC_TRUE); + } +} + +} // namespace + rac_result_t rac_telemetry_manager_flush(rac_telemetry_manager_t* manager) { if (!manager) { return RAC_ERROR_INVALID_ARGUMENT; @@ -1455,6 +1652,31 @@ rac_result_t rac_telemetry_manager_flush(rac_telemetry_manager_t* manager) { return RAC_SUCCESS; } + // Re-send batches whose earlier delivery failed once their backoff has + // elapsed — runs even when no new events are queued. + { + std::vector due; + { + std::lock_guard lock(manager->retry_mutex); + const int64_t now = get_current_timestamp_ms(); + auto it = manager->retry_queue.begin(); + while (it != manager->retry_queue.end()) { + if (it->next_attempt_ms <= now) { + due.push_back(std::move(*it)); + it = manager->retry_queue.erase(it); + } else { + ++it; + } + } + } + for (auto& batch : due) { + RAC_LOG_DEBUG("Telemetry", "Retrying telemetry batch (attempt %d): %s", + batch.attempts + 1, batch.endpoint.c_str()); + send_batch_json(manager, batch.endpoint, batch.json.c_str(), batch.json.size(), + batch.attempts); + } + } + // Get events from queue std::vector events; { @@ -1509,25 +1731,7 @@ rac_result_t rac_telemetry_manager_flush(rac_telemetry_manager_t* manager) { const std::string endpoint = std::string(RAC_ENDPOINT_TELEMETRY_V2_PREFIX) + modality; RAC_LOG_DEBUG("Telemetry", "POST %s (%zu bytes): %.500s", endpoint.c_str(), json_len, json); - if (manager->http_wakeup) { - // Isolate-safe path: enqueue an owned copy and signal the platform to - // drain it from its own thread/isolate (see poll_http_request). Used - // by Flutter, whose Dart FFI data callbacks are isolate-bound. - { - std::lock_guard lock(manager->http_queue_mutex); - while (manager->http_queue.size() >= rac_telemetry_manager::MAX_HTTP_QUEUE_SIZE) { - RAC_LOG_WARNING("Telemetry", - "HTTP queue full (%zu) — dropping oldest pending batch", - manager->http_queue.size()); - manager->http_queue.pop_front(); - } - manager->http_queue.push_back({endpoint, std::string(json, json_len), true}); - } - manager->http_wakeup(manager->http_wakeup_user_data); - } else if (manager->http_callback) { - manager->http_callback(manager->http_user_data, endpoint.c_str(), json, json_len, - RAC_TRUE); - } + send_batch_json(manager, endpoint, json, json_len, 0); free(json); } @@ -1558,12 +1762,42 @@ void rac_telemetry_manager_http_complete(rac_telemetry_manager_t* manager, rac_b if (!manager) return; + rac_telemetry_manager::PendingBatch batch; + bool have_batch = false; + { + std::lock_guard lock(manager->retry_mutex); + if (!manager->inflight.empty()) { + batch = std::move(manager->inflight.front()); + manager->inflight.pop_front(); + have_batch = true; + } + } + if (success == RAC_TRUE) { RAC_LOG_DEBUG("Telemetry", "Telemetry HTTP request completed successfully"); - } else { - RAC_LOG_WARNING("Telemetry", "Telemetry HTTP request failed: %s", - error_message ? error_message : "unknown"); + return; + } + + RAC_LOG_WARNING("Telemetry", "Telemetry HTTP request failed: %s", + error_message ? error_message : "unknown"); + if (!have_batch) { + return; } - // Could parse response and handle retries here if needed + batch.attempts += 1; + if (batch.attempts >= rac_telemetry_manager::MAX_SEND_ATTEMPTS) { + RAC_LOG_WARNING("Telemetry", "Dropping telemetry batch after %d failed attempts: %s", + batch.attempts, batch.endpoint.c_str()); + return; + } + + // Exponential backoff: 5s, 10s, ... — re-sent by a later flush. + batch.next_attempt_ms = get_current_timestamp_ms() + + (rac_telemetry_manager::RETRY_BASE_DELAY_MS << (batch.attempts - 1)); + std::lock_guard lock(manager->retry_mutex); + while (manager->retry_queue.size() >= rac_telemetry_manager::MAX_RETRY_QUEUE) { + RAC_LOG_WARNING("Telemetry", "Retry queue full — dropping oldest pending batch"); + manager->retry_queue.pop_front(); + } + manager->retry_queue.push_back(std::move(batch)); } diff --git a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_types.cpp b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_types.cpp index 79dfa3e771..4f2c5f08d7 100644 --- a/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_types.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_types.cpp @@ -18,6 +18,10 @@ rac_telemetry_payload_t rac_telemetry_payload_default(void) { payload.has_is_streaming = RAC_FALSE; payload.is_online = RAC_FALSE; payload.has_is_online = RAC_FALSE; + payload.battery_level = -1.0; + payload.is_low_power_mode = RAC_FALSE; + payload.has_is_low_power_mode = RAC_FALSE; + payload.cpu_usage_percent = -1.0; return payload; } diff --git a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp index 21600ac1f7..3898aff58e 100644 --- a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp +++ b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp @@ -61,6 +61,7 @@ #include "request_cancellation_relay.h" #include "../features/vlm/rac_vlm_lifecycle_bridge.h" +#include "../infrastructure/device/rac_device_live_state_internal.h" #include "../infrastructure/http/rac_http_internal.h" #include "rac/core/rac_audio_utils.h" #include "rac/core/rac_core.h" @@ -1618,6 +1619,9 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racInit(JNIEnv* env, jc LOGe("racInit failed with code: %d", result); } else { LOGi("racInit succeeded"); + // JNI callbacks attach the calling thread, so live battery/RAM + // sampling on telemetry events is safe from any thread here. + rac_telemetry_enable_live_platform_sampling(); } return static_cast(result); diff --git a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp index d7f85e0dae..1fe2ca0b14 100644 --- a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp +++ b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp @@ -111,6 +111,14 @@ bool environment_requires_external_config(rac_environment_t env) { bool http_setup_applicable_for_state() { const rac_environment_t env = rac_state_get_environment(); if (!environment_requires_external_config(env)) { + // Development with explicit credentials (api key + base URL from + // Phase 1) authenticates against the real backend, same contract as + // staging/production. The legacy Supabase-direct config remains the + // fallback for credential-free dev builds. + if (rac_dev_config_is_usable_http_url(rac_state_get_base_url()) && + rac_dev_config_is_usable_credential(rac_state_get_api_key())) { + return true; + } return rac_dev_config_is_usable_http_url(rac_dev_config_get_supabase_url()) && rac_dev_config_is_usable_credential(rac_dev_config_get_supabase_key()); } diff --git a/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp b/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp index 754136e027..0b5c580201 100644 --- a/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp +++ b/sdk/runanywhere-commons/tests/test_telemetry_extraction.cpp @@ -18,9 +18,11 @@ * bypassed and completion events flush synchronously into the capture callback. */ +#include #include #include #include +#include #include "rac/infrastructure/network/rac_environment.h" #include "rac/infrastructure/telemetry/rac_telemetry_manager.h" @@ -33,13 +35,13 @@ namespace v1 = runanywhere::v1; static int g_checks = 0; static int g_failures = 0; -#define CHECK(cond, msg) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_failures; \ - std::fprintf(stderr, " FAIL: %s\n", (msg)); \ - } \ +#define CHECK(cond, msg) \ + do { \ + ++g_checks; \ + if (!(cond)) { \ + ++g_failures; \ + std::fprintf(stderr, " FAIL: %s\n", (msg)); \ + } \ } while (0) #if defined(RAC_HAVE_PROTOBUF) @@ -160,7 +162,8 @@ int main() { CHECK(cap.endpoint == "/api/v2/sdk/telemetry/embeddings", "embeddings: routed correctly"); CHECK(has(cap.body, "\"total_tokens\":21"), "embeddings: total_tokens = 21"); CHECK(has(cap.body, "\"batch_size\":1"), "embeddings: batch_size = 1"); - CHECK(has(cap.body, "\"embedding_dimension\":384"), "embeddings: embedding_dimension = 384"); + CHECK(has(cap.body, "\"embedding_dimension\":384"), + "embeddings: embedding_dimension = 384"); } // --- LoRA failure: base_model_id + adapter_id + adapter_size_bytes ------ @@ -216,7 +219,7 @@ int main() { cap_ev->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_VLM_COMPLETED); cap_ev->set_component(v1::SDK_COMPONENT_VLM); cap_ev->set_model_id("smolvlm2-256m"); - cap_ev->set_input_count(1); // image count + cap_ev->set_input_count(1); // image count cap_ev->set_output_count(128); track(mgr, &cap, ev); CHECK(cap.called, "vlm: event delivered to sink"); @@ -225,6 +228,58 @@ int main() { CHECK(has(cap.body, "\"prompt_eval_time_ms\":826"), "vlm: prompt_eval_time_ms = 826"); } + // --- SDK origin + live device state stamped on every event -------------- + { + rac_client_info_t ci = {}; + ci.sdk_binding = "test-binding"; + rac_sdk_set_client_info(&ci); + + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_LLM); + auto* g = ev.mutable_generation(); + g->set_kind(v1::GENERATION_EVENT_KIND_COMPLETED); + g->set_model_id("qwen3-0.6b"); + track(mgr, &cap, ev); + CHECK(cap.called, "device-state: event delivered to sink"); + CHECK(has(cap.body, "\"sdk_binding\":\"test-binding\""), + "device-state: sdk_binding stamped"); + // No device-manager callbacks registered here, so battery/memory stay + // omitted; CPU core count is read in-process and must be present. + CHECK(has(cap.body, "\"online_core_count\":"), "device-state: online_core_count present"); + + rac_client_info_t reset = {}; + rac_sdk_set_client_info(&reset); + } + + // --- HTTP retry: a failed batch re-sends after backoff ------------------ + { + v1::SDKEvent ev; + envelope(&ev, v1::SDK_COMPONENT_LLM); + auto* g = ev.mutable_generation(); + g->set_kind(v1::GENERATION_EVENT_KIND_COMPLETED); + g->set_model_id("retry-model"); + + cap.called = false; + const std::string bytes = ev.SerializeAsString(); + rac_telemetry_manager_track_proto(mgr, reinterpret_cast(bytes.data()), + bytes.size()); + CHECK(cap.called, "retry: initial send delivered"); + rac_telemetry_manager_http_complete(mgr, RAC_FALSE, nullptr, "simulated network failure"); + + // Before the backoff elapses a flush must NOT re-send the batch. + cap.called = false; + rac_telemetry_manager_flush(mgr); + CHECK(!cap.called, "retry: no re-send before backoff"); + + std::this_thread::sleep_for(std::chrono::milliseconds(5200)); + cap.called = false; + rac_telemetry_manager_flush(mgr); + CHECK(cap.called, "retry: re-sent after backoff"); + CHECK(cap.endpoint == "/api/v2/sdk/telemetry/llm", "retry: same endpoint"); + CHECK(has(cap.body, "retry-model"), "retry: same batch body"); + rac_telemetry_manager_http_complete(mgr, RAC_TRUE, nullptr, nullptr); + } + rac_telemetry_manager_destroy(mgr); std::fprintf(stdout, " %d checks, %d failures\n", g_checks, g_failures); From d16b7e9b2ec3a6abfc4172e64c6f972a157ecb06 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Wed, 22 Jul 2026 01:16:30 +0530 Subject: [PATCH 21/44] sdks: real device info across kotlin/swift/flutter/rn/web (battery, SOC chip, gpu, core split, device names, stable fingerprint) + sdk_binding contract fixes --- .../lib/native/dart_bridge_device.dart | 242 +++++++++++++-- .../runanywhere/lib/public/runanywhere.dart | 23 +- .../packages/runanywhere/pubspec.yaml | 1 + .../sdk/foundation/bridge/CppBridge.kt | 8 +- .../bridge/extensions/CppBridgeDevice.kt | 143 +++++++-- .../bridge/extensions/CppBridgeHardware.kt | 161 +++++++++- .../core/android/src/main/cpp/cpp-adapter.cpp | 12 + .../runanywhere/PlatformAdapterBridge.kt | 118 +++++++ .../core/cpp/bridges/DeviceBridge.cpp | 3 +- .../core/cpp/bridges/DeviceBridge.hpp | 1 + .../packages/core/cpp/bridges/InitBridge.cpp | 290 ++++++++++++++++-- .../packages/core/cpp/bridges/InitBridge.hpp | 37 +++ .../packages/core/ios/PlatformAdapterBridge.h | 34 ++ .../packages/core/ios/PlatformAdapterBridge.m | 208 +++++++++++-- .../Bridge/Extensions/CppBridge+Device.swift | 2 +- .../Device/Models/Domain/DeviceInfo.swift | 197 ++++++++++-- .../src/Adapters/DeviceRegistrationAdapter.ts | 253 +++++++++++++-- 17 files changed, 1547 insertions(+), 186 deletions(-) diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart index d93251719f..334a791abb 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:io'; +import 'package:battery_plus/battery_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; @@ -482,6 +483,37 @@ class DartBridgeDevice { deviceId: _cachedDeviceId, ); } + _pushLiveDeviceState(); + } + + /// Push the freshly collected device state into the commons telemetry cache. + /// Telemetry stamping runs on inference threads where Dart FFI callbacks + /// must not be invoked; commons reads this pushed cache instead. + static void _pushLiveDeviceState() { + final snapshot = _cachedRegistrationInfo; + try { + final lib = PlatformLoader.loadCommons(); + final push = lib.lookupFunction< + Void Function(Double, Pointer, Int32, Int64, Int64), + void Function(double, Pointer, int, int, int) + >('rac_telemetry_push_live_device_state'); + final statePtr = snapshot.batteryState.isNotEmpty + ? snapshot.batteryState.toNativeUtf8() + : nullptr.cast(); + try { + push( + snapshot.batteryLevel, + statePtr, + snapshot.isLowPowerMode ? 1 : 0, + snapshot.totalMemory, + snapshot.availableMemory, + ); + } finally { + if (statePtr != nullptr) calloc.free(statePtr); + } + } catch (_) { + _logger.debug('Live device state push unavailable'); + } } // ============================================================================ @@ -929,6 +961,7 @@ class _DeviceRegistrationInfoSnapshot { Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { final deviceId = DartBridgeDevice._cachedDeviceId ?? ''; final plugin = DeviceInfoPlugin(); + final battery = await _collectBatterySnapshot(); if (Platform.isAndroid) { final info = await plugin.androidInfo; @@ -950,12 +983,12 @@ Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { chipName: chipName, totalMemory: _memoryMegabytesToBytes(info.physicalRamSize), availableMemory: _memoryMegabytesToBytes(info.availableRamSize), - hasNeuralEngine: false, + hasNeuralEngine: _androidHasNeuralEngine(chipName, info.manufacturer), neuralEngineCores: 0, gpuFamily: _inferAndroidGpuFamily(chipName, info.manufacturer), - batteryLevel: -1, - batteryState: '', - isLowPowerMode: false, + batteryLevel: battery.level, + batteryState: battery.state, + isLowPowerMode: battery.isLowPowerMode, coreCount: coreCount, performanceCores: coreSplit.$1, efficiencyCores: coreSplit.$2, @@ -967,8 +1000,9 @@ Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { final info = await plugin.iosInfo; final model = _nonEmpty(info.modelName) ?? info.model; final coreCount = Platform.numberOfProcessors; - final coreSplit = _coreDistribution(coreCount, model); final machine = _nonEmpty(info.utsname.machine) ?? 'unknown'; + final chipName = _appleChipName(machine); + final coreSplit = _coreDistribution(coreCount, model, chip: chipName); final hasNeuralEngine = info.isPhysicalDevice && (machine.startsWith('iPhone') || machine.startsWith('iPad')); @@ -981,15 +1015,17 @@ Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { _nonEmpty(info.systemVersion) ?? Platform.operatingSystemVersion, formFactor: model.toLowerCase().contains('ipad') ? 'tablet' : 'phone', architecture: _currentAbiArchitecture(), - chipName: machine, + chipName: chipName, totalMemory: _memoryMegabytesToBytes(info.physicalRamSize), availableMemory: _memoryMegabytesToBytes(info.availableRamSize), hasNeuralEngine: hasNeuralEngine, - neuralEngineCores: hasNeuralEngine ? 16 : 0, + neuralEngineCores: hasNeuralEngine + ? _appleNeuralEngineCores(chipName) + : 0, gpuFamily: 'apple', - batteryLevel: -1, - batteryState: '', - isLowPowerMode: false, + batteryLevel: battery.level, + batteryState: battery.state, + isLowPowerMode: battery.isLowPowerMode, coreCount: coreCount, performanceCores: coreSplit.$1, efficiencyCores: coreSplit.$2, @@ -1000,8 +1036,11 @@ Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { if (Platform.isMacOS) { final info = await plugin.macOsInfo; final model = _nonEmpty(info.modelName) ?? info.model; - final coreSplit = _coreDistribution(info.activeCPUs, model); final hasNeuralEngine = info.arch == 'arm64'; + final chipName = hasNeuralEngine + ? _appleChipName(_nonEmpty(info.model) ?? 'unknown') + : _nonEmpty(info.model) ?? 'unknown'; + final coreSplit = _coreDistribution(info.activeCPUs, model, chip: chipName); return _DeviceRegistrationInfoSnapshot( deviceId: deviceId, deviceModel: model, @@ -1011,15 +1050,17 @@ Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { '${info.majorVersion}.${info.minorVersion}.${info.patchVersion}', formFactor: 'desktop', architecture: _nonEmpty(info.arch) ?? 'unknown', - chipName: _nonEmpty(info.model) ?? 'unknown', + chipName: chipName, totalMemory: _memoryBytes(info.memorySize), + // device_info_plus exposes only total memory on macOS; 0 = unknown. availableMemory: 0, hasNeuralEngine: hasNeuralEngine, + // Every Apple Silicon Mac (M1 through M4) ships a 16-core ANE. neuralEngineCores: hasNeuralEngine ? 16 : 0, gpuFamily: 'apple', - batteryLevel: -1, - batteryState: '', - isLowPowerMode: false, + batteryLevel: battery.level, + batteryState: battery.state, + isLowPowerMode: battery.isLowPowerMode, coreCount: info.activeCPUs, performanceCores: coreSplit.$1, efficiencyCores: coreSplit.$2, @@ -1030,6 +1071,38 @@ Future<_DeviceRegistrationInfoSnapshot> _collectDeviceInfoSnapshot() async { return _DeviceRegistrationInfoSnapshot.defaults(deviceId: deviceId); } +typedef _BatterySnapshot = ({double level, String state, bool isLowPowerMode}); + +Future<_BatterySnapshot> _collectBatterySnapshot() async { + var level = -1.0; + var state = ''; + var isLowPowerMode = false; + final battery = Battery(); + try { + final percent = await battery.batteryLevel; + if (percent >= 0 && percent <= 100) { + level = percent / 100.0; + } + } catch (_) { + DartBridgeDevice._logger.debug('Battery level unavailable'); + } + try { + state = switch (await battery.batteryState) { + BatteryState.charging || BatteryState.connectedNotCharging => 'charging', + BatteryState.full => 'full', + BatteryState.discharging || BatteryState.unknown => 'unplugged', + }; + } catch (_) { + DartBridgeDevice._logger.debug('Battery state unavailable'); + } + try { + isLowPowerMode = await battery.isInBatterySaveMode; + } catch (_) { + DartBridgeDevice._logger.debug('Battery save mode unavailable'); + } + return (level: level, state: state, isLowPowerMode: isLowPowerMode); +} + String? _nonEmpty(String? value) { final normalized = value?.trim(); if (normalized == null || normalized.isEmpty) return null; @@ -1079,21 +1152,122 @@ int _memoryBytes(int bytes) { return bytes > 0 ? bytes : 0; } -(int, int) _coreDistribution(int coreCount, String model) { +(int, int) _coreDistribution(int coreCount, String model, {String chip = ''}) { if (coreCount <= 0) return (0, 0); - final lowerModel = model.toLowerCase(); int performance; - if (lowerModel.startsWith('iphone')) { - performance = 2; - } else if (lowerModel.startsWith('ipad') || lowerModel.startsWith('mac')) { - performance = (coreCount * 2 ~/ 5).clamp(2, coreCount).toInt(); + if (RegExp(r'^A\d').hasMatch(chip)) { + // A-series (A11-A19): 2 performance cores; A12X/A12Z: 4. + performance = chip.startsWith('A12X') || chip.startsWith('A12Z') ? 4 : 2; + } else if (RegExp(r'^M\d').hasMatch(chip)) { + // M-series: 4 efficiency cores on base/Pro/Max variants. + performance = coreCount - 4; + if (performance < 2) performance = coreCount ~/ 2; } else { - performance = (coreCount ~/ 3).clamp(1, coreCount).toInt(); + final lowerModel = model.toLowerCase(); + if (lowerModel.startsWith('iphone')) { + performance = 2; + } else if (lowerModel.startsWith('ipad') || lowerModel.startsWith('mac')) { + performance = (coreCount * 2 ~/ 5).clamp(2, coreCount).toInt(); + } else { + performance = (coreCount ~/ 3).clamp(1, coreCount).toInt(); + } } performance = performance.clamp(0, coreCount).toInt(); return (performance, coreCount - performance); } +const Map _appleExactChipById = { + 'iPhone17,1': 'A18 Pro', + 'iPhone17,2': 'A18 Pro', + 'iPhone18,1': 'A19 Pro', + 'iPhone18,2': 'A19 Pro', + 'iPhone18,4': 'A19 Pro', + 'iPad13,1': 'A14', + 'iPad13,2': 'A14', + 'iPad13,18': 'A14', + 'iPad13,19': 'A14', + 'iPad14,1': 'A15', + 'iPad14,2': 'A15', + 'iPad15,7': 'A16', + 'iPad15,8': 'A16', + 'iPad16,1': 'A17 Pro', + 'iPad16,2': 'A17 Pro', +}; + +const Map _appleChipByPrefix = { + 'iPhone10,': 'A11', + 'iPhone11,': 'A12', + 'iPhone12,': 'A13', + 'iPhone13,': 'A14', + 'iPhone14,': 'A15', + 'iPhone15,': 'A16', + 'iPhone16,': 'A17 Pro', + 'iPhone17,': 'A18', + 'iPhone18,': 'A19', + 'iPad7,': 'A10X', + 'iPad8,': 'A12X', + 'iPad11,': 'A12', + 'iPad12,': 'A13', + 'iPad13,': 'M1', + 'iPad14,': 'M2', + 'iPad15,': 'M3', + 'iPad16,': 'M4', + 'MacBookAir10,': 'M1', + 'MacBookPro17,': 'M1', + 'MacBookPro18,': 'M1', + 'Macmini9,': 'M1', + 'iMac21,': 'M1', + 'Mac13,': 'M1', + 'Mac14,': 'M2', + 'Mac15,': 'M3', + 'Mac16,': 'M4', +}; + +/// Map an Apple hardware identifier (e.g. "iPhone15,2", "Mac14,9") to a chip +/// family name. Falls back to the raw identifier when unmapped. +String _appleChipName(String machine) { + final exact = _appleExactChipById[machine]; + if (exact != null) return exact; + for (final entry in _appleChipByPrefix.entries) { + if (machine.startsWith(entry.key)) return entry.value; + } + return machine; +} + +/// ANE core counts per chip family: A11=2, A12/A13=8, A14+ and M-series=16. +/// Unmapped chips report 0 (unknown). +int _appleNeuralEngineCores(String chip) { + if (chip == 'A11') return 2; + if (chip == 'A12' || chip == 'A12X' || chip == 'A13') return 8; + if (RegExp(r'^M\d').hasMatch(chip)) return 16; + final generation = RegExp(r'^A1(\d)').firstMatch(chip); + if (generation != null && int.parse(generation.group(1)!) >= 4) return 16; + return 0; +} + +/// NPU heuristic from the SoC/hardware string: Snapdragon SM8/SM7/QCM, +/// Google Tensor, Exynos 2xxx (s5e9xxx), MediaTek Dimensity. +bool _androidHasNeuralEngine(String chipName, String manufacturer) { + final chip = chipName.toLowerCase(); + if (RegExp(r'sm[78]\d{3}').hasMatch(chip) || chip.contains('qcm')) { + return true; + } + if (chip.contains('tensor') || + RegExp(r'\bgs\d{3}').hasMatch(chip) || + chip.contains('zuma') || + manufacturer.toLowerCase().contains('google')) { + return true; + } + if (RegExp(r'exynos\s?2\d{3}').hasMatch(chip) || + RegExp(r's5e9[89]\d{2}').hasMatch(chip)) { + return true; + } + if (chip.contains('dimensity') || RegExp(r'mt6[89]\d{2}').hasMatch(chip)) { + return true; + } + return false; +} + String _defaultFormFactor() { if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { return 'desktop'; @@ -1120,24 +1294,30 @@ String _inferAndroidGpuFamily(String chipName, String manufacturer) { final maker = manufacturer.toLowerCase(); if (chip.contains('snapdragon') || chip.contains('qualcomm') || + chip.contains('qcom') || + chip.contains('qcm') || chip.contains('sdm') || - chip.contains('sm8') || - chip.contains('sm7') || - chip.contains('sm6') || + RegExp(r'sm[4-8]\d{3}').hasMatch(chip) || chip.contains('msm') || maker.contains('qualcomm')) { return 'adreno'; } - if (chip.contains('exynos') || - chip.contains('tensor') || - chip.contains('mediatek') || + // Exynos 2200+ (s5e992x/s5e994x) use AMD Xclipse; earlier Exynos use Mali. + if (RegExp(r's5e9(9[24]|4\d)\d').hasMatch(chip)) return 'xclipse'; + if (chip.contains('exynos') || chip.contains('s5e')) return 'mali'; + if (chip.contains('tensor') || + RegExp(r'\bgs\d{3}').hasMatch(chip) || + chip.contains('zuma') || + maker.contains('google')) { + return 'mali'; + } + if (chip.contains('mediatek') || chip.contains('dimensity') || chip.contains('helio') || - chip.contains('kirin') || - maker.contains('google') || - maker.contains('samsung')) { + RegExp(r'\bmt\d{4}').hasMatch(chip)) { return 'mali'; } + if (chip.contains('kirin') || maker.contains('samsung')) return 'mali'; if (chip.contains('intel')) return 'intel'; if (chip.contains('nvidia') || chip.contains('tegra')) return 'nvidia'; return 'unknown'; diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index 8e92386a59..57e97e2a3a 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -486,11 +486,26 @@ abstract final class RunAnywhere { }) async { final SDKInitParams params; - if (environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { - // Development mode ignores any caller-supplied baseURL and always uses - // the dev placeholder / Supabase-derived URL. Mirrors Swift - // RunAnywhere.swift:125-127 (`SDKInitParams(forDevelopmentWithAPIKey:)`). + if (environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT && + (baseURL == null || baseURL.isEmpty)) { + // Development without an explicit baseURL falls back to the dev + // placeholder / Supabase-derived URL. A caller-supplied baseURL is + // honored so dev builds can target a real backend (the placeholder + // DNS alias is unreachable on most machines). params = SDKInitParams.forDevelopment(apiKey: apiKey ?? ''); + } else if (environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { + final parsed = Uri.tryParse(baseURL!); + if (parsed == null) { + throw SDKException.validationFailed( + 'Invalid base URL: $baseURL', + fieldPath: 'SDKInitParams.baseURL', + ); + } + params = SDKInitParams( + apiKey: apiKey ?? '', + baseURL: parsed, + environment: environment, + ); } else { // Keyless staging is valid: commons overrides the base URL with the // baked staging backend and requests go out unauthenticated diff --git a/sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml b/sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml index 525fcec06b..088474f1ed 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml +++ b/sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: # The cap originated when 11.3.x introduced a transient ABI/Manifest issue; # 13.x is now the latest stable and the cap is no longer warranted. device_info_plus: ^13.1.0 + battery_plus: ^7.0.0 flutter_timezone: ^5.1.0 package_info_plus: ^10.1.0 # Utilities diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt index 36b4178f8f..ed45e6e306 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt @@ -395,9 +395,11 @@ object CppBridge { } else { // Read the effective config from commons state: staging // overrides whatever the app passed (baked URL, keyless). - val baseUrl = RunAnywhereBridge.racStateGetBaseUrl() - ?.takeIf { it.isNotEmpty() } - ?: CppBridgeTelemetry.getBaseUrl() + val baseUrl = + RunAnywhereBridge + .racStateGetBaseUrl() + ?.takeIf { it.isNotEmpty() } + ?: CppBridgeTelemetry.getBaseUrl() val apiKey = CppBridgeTelemetry.getApiKey() if (!baseUrl.isNullOrEmpty()) { HTTPClientAdapter.configure(baseUrl, apiKey) diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt index f5e94afdae..a4ea40f51c 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt @@ -32,8 +32,17 @@ package com.runanywhere.sdk.foundation.bridge.extensions import ai.runanywhere.proto.v1.DeviceInfo +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.os.BatteryManager +import android.os.Build +import android.os.PowerManager +import android.provider.Settings import com.runanywhere.sdk.foundation.bridge.HTTPClientAdapter import com.runanywhere.sdk.foundation.errors.SDKException +import com.runanywhere.sdk.foundation.security.AndroidPlatformContext import com.runanywhere.sdk.native.bridge.RunAnywhereBridge import com.runanywhere.sdk.public.configuration.SDKEnvironment import kotlinx.coroutines.runBlocking @@ -236,7 +245,7 @@ object CppBridgeDevice { val provider = deviceInfoProvider val deviceModel = provider?.getDeviceModel() ?: getDefaultDeviceModel() - val deviceName = provider?.getDeviceName() ?: deviceModel + val deviceName = provider?.getDeviceName() ?: getDefaultDeviceName(deviceModel) val manufacturer = provider?.getDeviceManufacturer() ?: getDefaultManufacturer() val osVersion = provider?.getOSVersion() ?: getDefaultOsVersion() val osBuildId = provider?.getOSBuildId() ?: "" @@ -248,20 +257,21 @@ object CppBridgeDevice { .getDefault() .id val isEmulator = provider?.isEmulator() ?: false - val formFactor = provider?.getFormFactor() ?: "phone" + val formFactor = provider?.getFormFactor() ?: getDefaultFormFactor() val architecture = provider?.getArchitecture() ?: CppBridgeHardware.defaultArchitecture() val chipName = provider?.getChipName() ?: CppBridgeHardware.defaultChipName(architecture) val totalMemory = provider?.getTotalMemory() ?: CppBridgeHardware.defaultTotalMemory() - val availableMemory = provider?.getAvailableMemory() ?: (totalMemory / 2) - val hasNeuralEngine = provider?.hasNeuralEngine() ?: false + val availableMemory = provider?.getAvailableMemory() ?: CppBridgeHardware.defaultAvailableMemory(totalMemory) + val hasNeuralEngine = provider?.hasNeuralEngine() ?: CppBridgeHardware.defaultHasNeuralEngine(chipName) val neuralEngineCores = provider?.getNeuralEngineCores() ?: 0 val gpuFamily = provider?.getGPUFamily() ?: CppBridgeHardware.defaultGpuFamily(chipName) - val batteryLevel = provider?.getBatteryLevel() ?: -1.0 - val batteryState = provider?.getBatteryState() - val isLowPowerMode = provider?.isLowPowerMode() ?: false + val batteryLevel = provider?.getBatteryLevel() ?: getDefaultBatteryLevel() + val batteryState = provider?.getBatteryState() ?: getDefaultBatteryState() + val isLowPowerMode = provider?.isLowPowerMode() ?: getDefaultIsLowPowerMode() val coreCount = provider?.getCoreCount() ?: Runtime.getRuntime().availableProcessors() - val performanceCores = provider?.getPerformanceCores() ?: (coreCount / 2) - val efficiencyCores = provider?.getEfficiencyCores() ?: (coreCount - performanceCores) + val defaultCoreSplit = CppBridgeHardware.defaultCoreSplit(coreCount) + val performanceCores = provider?.getPerformanceCores() ?: defaultCoreSplit.first + val efficiencyCores = provider?.getEfficiencyCores() ?: defaultCoreSplit.second val deviceIdValue = deviceId ?: "" val deviceInfo = @@ -480,37 +490,114 @@ object CppBridgeDevice { ) } - /** Android-specific Build.MODEL fallback via reflection. */ - private fun getDefaultDeviceModel(): String = + /** Application context, or null when the SDK has no context yet. */ + private fun appContextOrNull(): Context? = try { - Class.forName("android.os.Build").getField("MODEL").get(null) as? String ?: "unknown" + if (AndroidPlatformContext.isInitialized()) { + AndroidPlatformContext.applicationContext + } else { + null + } } catch (_: Exception) { - System.getProperty("os.name") ?: "unknown" + null } - /** Android-specific Build.MANUFACTURER fallback via reflection. */ - private fun getDefaultManufacturer(): String = - try { - Class.forName("android.os.Build").getField("MANUFACTURER").get(null) as? String ?: "unknown" + /** Manufacturer-prefixed model, e.g. "Nothing A059". */ + private fun getDefaultDeviceModel(): String { + val model = Build.MODEL?.takeIf { it.isNotBlank() } ?: "unknown" + val manufacturer = Build.MANUFACTURER?.takeIf { it.isNotBlank() } ?: return model + if (model.contains(manufacturer, ignoreCase = true)) return model + val prefix = manufacturer.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } + return "$prefix $model" + } + + /** + * User-facing device name: `Settings.Global.DEVICE_NAME` (API 25+), + * then the Bluetooth name, then the manufacturer-prefixed model. + */ + private fun getDefaultDeviceName(deviceModel: String): String { + val context = appContextOrNull() + if (context != null) { + try { + val resolver = context.contentResolver + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + Settings.Global + .getString(resolver, Settings.Global.DEVICE_NAME) + ?.takeIf { it.isNotBlank() } + ?.let { return it } + } + Settings.Secure + .getString(resolver, "bluetooth_name") + ?.takeIf { it.isNotBlank() } + ?.let { return it } + } catch (_: Exception) { + // Fall through to model + } + } + return deviceModel + } + + private fun getDefaultManufacturer(): String = Build.MANUFACTURER?.takeIf { it.isNotBlank() } ?: "unknown" + + private fun getDefaultOsVersion(): String = Build.VERSION.RELEASE?.takeIf { it.isNotBlank() } ?: "unknown" + + private fun getDefaultSdkVersion(): Int = Build.VERSION.SDK_INT + + /** Derive form factor from UI mode (tv) and screen layout size (tablet). */ + private fun getDefaultFormFactor(): String { + val context = appContextOrNull() ?: return "phone" + return try { + val config = context.resources.configuration + val screenSize = config.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK + when { + (config.uiMode and Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION -> "tv" + screenSize >= Configuration.SCREENLAYOUT_SIZE_LARGE -> "tablet" + else -> "phone" + } } catch (_: Exception) { - System.getProperty("java.vendor") ?: "unknown" + "phone" } + } - /** Android-specific Build.VERSION.RELEASE fallback via reflection. */ - private fun getDefaultOsVersion(): String = - try { - Class.forName("android.os.Build\$VERSION").getField("RELEASE").get(null) as? String ?: "unknown" + /** Battery level in 0.0–1.0, or -1.0 when unavailable. */ + private fun getDefaultBatteryLevel(): Double { + val context = appContextOrNull() ?: return -1.0 + return try { + val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager ?: return -1.0 + val percent = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + if (percent in 0..100) percent / 100.0 else -1.0 } catch (_: Exception) { - System.getProperty("os.version") ?: "unknown" + -1.0 } + } - /** Android-specific Build.VERSION.SDK_INT fallback via reflection. */ - private fun getDefaultSdkVersion(): Int = - try { - Class.forName("android.os.Build\$VERSION").getField("SDK_INT").get(null) as? Int ?: 0 + /** "charging" / "full" / "unplugged" from the sticky battery intent. */ + private fun getDefaultBatteryState(): String? { + val context = appContextOrNull() ?: return null + return try { + val intent = + context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) ?: return null + when (intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)) { + BatteryManager.BATTERY_STATUS_CHARGING -> "charging" + BatteryManager.BATTERY_STATUS_FULL -> "full" + BatteryManager.BATTERY_STATUS_DISCHARGING, + BatteryManager.BATTERY_STATUS_NOT_CHARGING, + -> "unplugged" + else -> null + } } catch (_: Exception) { - 0 + null } + } + + private fun getDefaultIsLowPowerMode(): Boolean { + val context = appContextOrNull() ?: return false + return try { + (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode ?: false + } catch (_: Exception) { + false + } + } /** Escape special chars for embedding in a JSON string value. */ private fun escapeJson(value: String): String = diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeHardware.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeHardware.kt index 2d46076879..96c78d46fb 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeHardware.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeHardware.kt @@ -16,6 +16,12 @@ package com.runanywhere.sdk.foundation.bridge.extensions +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import com.runanywhere.sdk.foundation.security.AndroidPlatformContext +import java.util.Locale + /** * Hardware profile bridge wrapping the `rac_hardware_profile_*` ABI. * @@ -131,14 +137,34 @@ object CppBridgeHardware { /** * Get default chip name based on architecture and device info. * - * Tries to read from `Build.HARDWARE` and `/proc/cpuinfo`. + * On API 31+ prefers `Build.SOC_MODEL` (+ `Build.SOC_MANUFACTURER` prefix, + * e.g. "Qualcomm SM7635"). Below 31 falls back to `Build.HARDWARE` and + * `/proc/cpuinfo`. Vendor-only values ("qcom") are never returned bare. */ fun defaultChipName(architecture: String): String { - // Try to get from Build.HARDWARE + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + val socModel = Build.SOC_MODEL.takeIf { it.isNotBlank() && !it.equals(Build.UNKNOWN, ignoreCase = true) } + if (socModel != null) { + val socManufacturer = + Build.SOC_MANUFACTURER.takeIf { + it.isNotBlank() && !it.equals(Build.UNKNOWN, ignoreCase = true) + } + return if (socManufacturer != null && !socModel.contains(socManufacturer, ignoreCase = true)) { + "$socManufacturer $socModel" + } else { + socModel + } + } + } catch (e: Exception) { + // Fall through + } + } + + // Try to get from Build.HARDWARE (skip bare vendor strings like "qcom") try { - val buildClass = Class.forName("android.os.Build") - val hardware = buildClass.getField("HARDWARE").get(null) as? String - if (!hardware.isNullOrEmpty() && hardware != "unknown") { + val hardware = Build.HARDWARE + if (!hardware.isNullOrEmpty() && hardware != "unknown" && !hardware.equals("qcom", ignoreCase = true)) { return hardware } } catch (e: Exception) { @@ -152,7 +178,7 @@ object CppBridgeHardware { val hardwareLine = cpuInfo.lines().find { it.startsWith("Hardware", ignoreCase = true) } if (hardwareLine != null) { val chipName = hardwareLine.substringAfter(":").trim() - if (chipName.isNotEmpty()) { + if (chipName.isNotEmpty() && !chipName.equals("qcom", ignoreCase = true)) { return chipName } } @@ -176,6 +202,25 @@ object CppBridgeHardware { * - Apple -> Apple */ fun defaultGpuFamily(chipName: String): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + val socManufacturer = Build.SOC_MANUFACTURER.lowercase(Locale.ROOT) + val socModel = Build.SOC_MODEL.lowercase(Locale.ROOT) + when { + socManufacturer.contains("qualcomm") -> return "adreno" + // Exynos 2200+ ship AMD Xclipse; earlier Exynos use Mali + socManufacturer.contains("samsung") -> + return if (Regex("2[2-9]\\d\\d").containsMatchIn(socModel)) "xclipse" else "mali" + socManufacturer.contains("google") -> return "mali" + // Dimensity 9xxx flagships ship Immortalis; the rest use Mali + socManufacturer.contains("mediatek") -> + return if (Regex("9\\d{3}").containsMatchIn(socModel)) "immortalis" else "mali" + } + } catch (e: Exception) { + // Fall through to chip-name inference + } + } + val chipLower = chipName.lowercase() return when { @@ -219,4 +264,108 @@ object CppBridgeHardware { else -> "unknown" } } + + /** + * Get currently available (free) memory in bytes. + * + * Uses `ActivityManager.MemoryInfo.availMem`; falls back to + * `/proc/meminfo` `MemAvailable`, then to `totalMemory / 2`. + */ + fun defaultAvailableMemory(totalMemory: Long): Long { + try { + val context = + if (AndroidPlatformContext.isInitialized()) { + AndroidPlatformContext.applicationContext + } else { + null + } + val activityManager = context?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + if (activityManager != null) { + val memInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memInfo) + if (memInfo.availMem > 0) { + return memInfo.availMem + } + } + } catch (e: Exception) { + // Fall through to /proc/meminfo + } + + try { + java.io.File("/proc/meminfo").useLines { lines -> + val memAvailable = lines.find { it.startsWith("MemAvailable:") } + val kb = + memAvailable + ?.substringAfter(":") + ?.trim() + ?.removeSuffix(" kB") + ?.trim() + ?.toLongOrNull() + if (kb != null && kb > 0) { + return kb * 1024L + } + } + } catch (e: Exception) { + // Fall through + } + + return totalMemory / 2 + } + + /** + * Heuristic NPU presence check: Qualcomm 8/7-series Hexagon, Google + * Tensor, Exynos 2xxx, and MediaTek Dimensity all ship dedicated NPUs. + */ + fun defaultHasNeuralEngine(chipName: String): Boolean { + val soc = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + "${Build.SOC_MANUFACTURER} ${Build.SOC_MODEL}" + } catch (e: Exception) { + "" + } + } else { + "" + } + val haystack = "$soc $chipName".lowercase(Locale.ROOT) + return haystack.contains("sm8") || + haystack.contains("sm7") || + haystack.contains("qcm") || + haystack.contains("tensor") || + haystack.contains("gs1") || + haystack.contains("gs2") || + haystack.contains("dimensity") || + Regex("(exynos|s5e)\\s*2\\d{3}").containsMatchIn(haystack) + } + + /** + * Split cores into (performance, efficiency) by reading per-core + * `cpuinfo_max_freq` sysfs entries: cores at the shared maximum + * frequency count as performance cores. Falls back to a half/half + * split when sysfs is unreadable. + */ + fun defaultCoreSplit(coreCount: Int): Pair { + if (coreCount > 0) { + try { + val freqs = + (0 until coreCount).mapNotNull { cpu -> + try { + val sysfs = java.io.File("/sys/devices/system/cpu/cpu$cpu/cpufreq/cpuinfo_max_freq") + sysfs.readText().trim().toLongOrNull() + } catch (e: Exception) { + null + } + } + if (freqs.size == coreCount) { + val maxFreq = freqs.max() + val performance = freqs.count { it == maxFreq } + return performance to (coreCount - performance) + } + } catch (e: Exception) { + // Fall through to heuristic + } + } + val performance = coreCount / 2 + return performance to (coreCount - performance) + } } diff --git a/sdk/runanywhere-react-native/packages/core/android/src/main/cpp/cpp-adapter.cpp b/sdk/runanywhere-react-native/packages/core/android/src/main/cpp/cpp-adapter.cpp index 2d2c41eee9..decfb31d1e 100644 --- a/sdk/runanywhere-react-native/packages/core/android/src/main/cpp/cpp-adapter.cpp +++ b/sdk/runanywhere-react-native/packages/core/android/src/main/cpp/cpp-adapter.cpp @@ -21,6 +21,11 @@ jmethodID g_secureGetMethod = nullptr; jmethodID g_secureDeleteMethod = nullptr; jmethodID g_getModelBaseDirectoryMethod = nullptr; jmethodID g_getDeviceModelMethod = nullptr; +jmethodID g_getDeviceNameMethod = nullptr; +jmethodID g_getBatteryLevelMethod = nullptr; +jmethodID g_getBatteryStateMethod = nullptr; +jmethodID g_isLowPowerModeMethod = nullptr; +jmethodID g_hasNPUMethod = nullptr; jmethodID g_getOSVersionMethod = nullptr; jmethodID g_getChipNameMethod = nullptr; jmethodID g_getTotalMemoryMethod = nullptr; @@ -63,6 +68,11 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { "(Ljava/lang/String;)Z"); g_getModelBaseDirectoryMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getModelBaseDirectory", "()Ljava/lang/String;"); g_getDeviceModelMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getDeviceModel", "()Ljava/lang/String;"); + g_getDeviceNameMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getDeviceName", "()Ljava/lang/String;"); + g_getBatteryLevelMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getBatteryLevel", "()F"); + g_getBatteryStateMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getBatteryState", "()Ljava/lang/String;"); + g_isLowPowerModeMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "isLowPowerMode", "()Z"); + g_hasNPUMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "hasNPU", "()Z"); g_getOSVersionMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getOSVersion", "()Ljava/lang/String;"); g_getChipNameMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getChipName", "()Ljava/lang/String;"); g_getTotalMemoryMethod = env->GetStaticMethodID(g_platformAdapterBridgeClass, "getTotalMemory", "()J"); @@ -89,6 +99,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { if (g_secureSetMethod && g_secureGetMethod && g_secureDeleteMethod && g_getModelBaseDirectoryMethod && g_fileListDirectoryMethod && g_isNonEmptyDirectoryMethod && g_getDeviceModelMethod && + g_getDeviceNameMethod && g_getBatteryLevelMethod && + g_getBatteryStateMethod && g_isLowPowerModeMethod && g_hasNPUMethod && g_getOSVersionMethod && g_getChipNameMethod && g_getTotalMemoryMethod && g_getAvailableMemoryMethod && g_getCoreCountMethod && g_getArchitectureMethod && diff --git a/sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt b/sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt index b9ceebc68d..5ccf5e8b4f 100644 --- a/sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt +++ b/sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt @@ -11,9 +11,14 @@ package com.margelo.nitro.runanywhere import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.os.BatteryManager import android.os.Build +import android.os.PowerManager +import android.provider.Settings import android.util.Log import com.margelo.nitro.NitroModules import java.io.File @@ -285,6 +290,119 @@ object PlatformAdapterBridge { return android.os.Build.MODEL } + /** + * Get user-visible device name (Settings.Global.DEVICE_NAME on API 25+, + * falling back to bluetooth_name, then "MANUFACTURER MODEL"). + */ + @JvmStatic + fun getDeviceName(): String { + val context = applicationContext() + if (context != null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) + ?.takeIf { it.isNotBlank() } + ?.let { return it } + } + Settings.Secure.getString(context.contentResolver, "bluetooth_name") + ?.takeIf { it.isNotBlank() } + ?.let { return it } + } catch (e: Exception) { + Log.w(TAG, "getDeviceName failed: ${e.message}") + } + } + return "${Build.MANUFACTURER} ${Build.MODEL}" + } + + /** + * Get battery level as 0.0..1.0, or -1.0 when unknown + */ + @JvmStatic + fun getBatteryLevel(): Float { + val context = applicationContext() ?: return -1.0f + return try { + val batteryManager = + context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager + ?: return -1.0f + val level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + if (level in 0..100) level / 100.0f else -1.0f + } catch (e: Exception) { + Log.w(TAG, "getBatteryLevel failed: ${e.message}") + -1.0f + } + } + + /** + * Get battery state: "charging", "full", "unplugged", or "" when unknown + */ + @JvmStatic + fun getBatteryState(): String { + val context = applicationContext() ?: return "" + return try { + val intent = context.registerReceiver( + null, + IntentFilter(Intent.ACTION_BATTERY_CHANGED), + ) ?: return "" + when ( + intent.getIntExtra( + BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN, + ) + ) { + BatteryManager.BATTERY_STATUS_CHARGING -> "charging" + BatteryManager.BATTERY_STATUS_FULL -> "full" + BatteryManager.BATTERY_STATUS_DISCHARGING, + BatteryManager.BATTERY_STATUS_NOT_CHARGING, + -> "unplugged" + else -> "" + } + } catch (e: Exception) { + Log.w(TAG, "getBatteryState failed: ${e.message}") + "" + } + } + + /** + * Check if battery saver (low power mode) is enabled + */ + @JvmStatic + fun isLowPowerMode(): Boolean { + val context = applicationContext() ?: return false + return try { + val powerManager = + context.getSystemService(Context.POWER_SERVICE) as? PowerManager + powerManager?.isPowerSaveMode ?: false + } catch (e: Exception) { + Log.w(TAG, "isLowPowerMode failed: ${e.message}") + false + } + } + + /** + * Check if the SoC ships a dedicated NPU/DSP usable through NNAPI. + * Mirrors HybridRunAnywhereDeviceInfo.hasNPU(). + */ + @JvmStatic + fun hasNPU(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { + return false + } + val hardware = Build.HARDWARE.lowercase() + val soc = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Build.SOC_MODEL.lowercase() + } else { + "" + } + return listOf( + "qcom", + "exynos", + "tensor", + "kirin", + "dimensity", + "mtk", + ).any { hardware.contains(it) || soc.contains(it) } + } + /** * Get OS version (e.g., "14") */ diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.cpp index cccfc3204a..46ffb8633b 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.cpp @@ -101,7 +101,8 @@ static void deviceGetInfoCallback(rac_device_registration_info_t* outInfo, void* g_deviceCallbackStrings.chipName = info.chipName; g_deviceCallbackStrings.gpuFamily = info.gpuFamily; g_deviceCallbackStrings.batteryState = info.batteryState; - g_deviceCallbackStrings.deviceFingerprint = info.deviceId; + g_deviceCallbackStrings.deviceFingerprint = + info.deviceFingerprint.empty() ? info.deviceId : info.deviceFingerprint; // Fill out the struct - matches Swift's implementation outInfo->device_id = g_deviceCallbackStrings.deviceId.c_str(); diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.hpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.hpp index f1e009f062..59e8c72153 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.hpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/DeviceBridge.hpp @@ -47,6 +47,7 @@ struct DeviceInfo { int32_t efficiencyCores = 0; bool isSimulator = false; std::string sdkVersion; + std::string deviceFingerprint; }; /** diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp index e3041d697c..9e0f8ae692 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp @@ -13,6 +13,7 @@ #include "HTTPBridge.hpp" #include "PlatformDownloadBridge.h" #include "rac/foundation/rac_proto_buffer.h" +#include "rac/foundation/rac_sha256.h" #include "rac/infrastructure/device/rac_device_identity.h" // rac_device_get_or_create_persistent_id #include "rac/infrastructure/http/rac_http_client.h" #include "rac/infrastructure/model_management/rac_model_paths.h" @@ -62,6 +63,11 @@ extern jmethodID g_secureGetMethod; extern jmethodID g_secureDeleteMethod; extern jmethodID g_getModelBaseDirectoryMethod; extern jmethodID g_getDeviceModelMethod; +extern jmethodID g_getDeviceNameMethod; +extern jmethodID g_getBatteryLevelMethod; +extern jmethodID g_getBatteryStateMethod; +extern jmethodID g_isLowPowerModeMethod; +extern jmethodID g_hasNPUMethod; extern jmethodID g_getOSVersionMethod; extern jmethodID g_getChipNameMethod; extern jmethodID g_getTotalMemoryMethod; @@ -445,6 +451,68 @@ namespace AndroidBridge { return result == JNI_TRUE; } + std::string getDeviceName() { + return callStaticString(g_getDeviceNameMethod, "getDeviceName"); + } + + float getBatteryLevel() { + JNIEnv* env = getJNIEnv(); + if (!env) return -1.0f; + + if (!g_platformAdapterBridgeClass || !g_getBatteryLevelMethod) { + LOGE("PlatformAdapterBridge class or getBatteryLevel method not cached"); + return -1.0f; + } + + jfloat result = env->CallStaticFloatMethod(g_platformAdapterBridgeClass, g_getBatteryLevelMethod); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LOGE("Exception in PlatformAdapterBridge.getBatteryLevel"); + return -1.0f; + } + return static_cast(result); + } + + std::string getBatteryState() { + return callStaticString(g_getBatteryStateMethod, "getBatteryState"); + } + + bool isLowPowerMode() { + JNIEnv* env = getJNIEnv(); + if (!env) return false; + + if (!g_platformAdapterBridgeClass || !g_isLowPowerModeMethod) { + LOGE("PlatformAdapterBridge class or isLowPowerMode method not cached"); + return false; + } + + jboolean result = env->CallStaticBooleanMethod(g_platformAdapterBridgeClass, g_isLowPowerModeMethod); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LOGE("Exception in PlatformAdapterBridge.isLowPowerMode"); + return false; + } + return result == JNI_TRUE; + } + + bool hasNPU() { + JNIEnv* env = getJNIEnv(); + if (!env) return false; + + if (!g_platformAdapterBridgeClass || !g_hasNPUMethod) { + LOGE("PlatformAdapterBridge class or hasNPU method not cached"); + return false; + } + + jboolean result = env->CallStaticBooleanMethod(g_platformAdapterBridgeClass, g_hasNPUMethod); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LOGE("Exception in PlatformAdapterBridge.hasNPU"); + return false; + } + return result == JNI_TRUE; + } + std::string getAppIdentifier() { return callStaticString(g_getAppIdentifierMethod, "getAppIdentifier"); } @@ -652,6 +720,12 @@ extern "C" { int PlatformAdapter_getCoreCount(void); bool PlatformAdapter_getArchitecture(char** outValue); bool PlatformAdapter_getGPUFamily(char** outValue); + bool PlatformAdapter_getDeviceName(char** outValue); + float PlatformAdapter_getBatteryLevel(void); + bool PlatformAdapter_getBatteryState(char** outValue); + bool PlatformAdapter_isLowPowerMode(void); + int PlatformAdapter_getPerformanceCores(void); + int PlatformAdapter_getEfficiencyCores(void); // App/client metadata (Bundle.main) bool PlatformAdapter_getAppIdentifier(char** outValue); @@ -713,6 +787,35 @@ template void wipeAndClear(Container &value) { value.clear(); } +#if defined(__APPLE__) +// Apple Neural Engine core counts by chip generation: A11 = 2, A12/A13 = 8, +// A14+ and all M-series = 16. Unknown chips report 0 rather than a guess. +int neuralEngineCoresForChip(const std::string& chipName) { + if (chipName.size() < 2) { + return 0; + } + const char family = chipName[0]; + const char second = chipName[1]; + if ((family != 'A' && family != 'M') || second < '0' || second > '9') { + return 0; + } + const int generation = std::atoi(chipName.c_str() + 1); + if (family == 'M') { + return 16; + } + if (generation >= 14) { + return 16; + } + if (generation == 12 || generation == 13) { + return 8; + } + if (generation == 11) { + return 2; + } + return 0; +} +#endif + } // anonymous namespace // ============================================================================= @@ -898,7 +1001,7 @@ static std::string getClientTimezone() { } static void configureClientInfo() { - const std::string sdkBinding = "react_native"; + const std::string sdkBinding = "react-native"; const std::string appIdentifier = getClientAppIdentifier(); const std::string appName = getClientAppName(); const std::string appVersion = getClientAppVersion(); @@ -1867,7 +1970,10 @@ rac_result_t InitBridge::registerDeviceCallbacks() { #endif info.sdkVersion = InitBridge::shared().getSdkVersion(); info.deviceModel = InitBridge::shared().getDeviceModel(); - info.deviceName = info.deviceModel; + info.deviceName = InitBridge::shared().getDeviceName(); + if (info.deviceName.empty()) { + info.deviceName = info.deviceModel; + } info.osVersion = InitBridge::shared().getOSVersion(); info.chipName = InitBridge::shared().getChipName(); info.architecture = InitBridge::shared().getArchitecture(); @@ -1876,34 +1982,48 @@ rac_result_t InitBridge::registerDeviceCallbacks() { info.coreCount = InitBridge::shared().getCoreCount(); info.gpuFamily = InitBridge::shared().getGPUFamily(); info.formFactor = InitBridge::shared().isTablet() ? "tablet" : "phone"; - info.batteryLevel = -1.0f; - info.batteryState = ""; - info.isLowPowerMode = false; - // Mirrors Swift DeviceInfo.swift: Neural Engine is derived from the - // architecture (arm64 Apple silicon), never hardcoded — x86 simulators - // report none. Cores follow Swift's `hasNeuralEngine ? 16 : 0`. + info.batteryLevel = InitBridge::shared().getBatteryLevel(); + info.batteryState = InitBridge::shared().getBatteryState(); + info.isLowPowerMode = InitBridge::shared().isLowPowerMode(); + // Apple: Neural Engine derived from the architecture (arm64 Apple + // silicon) with per-generation core counts; x86 simulators report + // none. Android: NPU presence detected from the SoC family; core + // counts are not exposed, so they stay 0 rather than a guess. + info.hasNeuralEngine = InitBridge::shared().hasNeuralEngine(); #if defined(__APPLE__) - info.hasNeuralEngine = info.architecture == "arm64"; + info.neuralEngineCores = + info.hasNeuralEngine ? neuralEngineCoresForChip(info.chipName) : 0; #else - info.hasNeuralEngine = false; + info.neuralEngineCores = 0; #endif - info.neuralEngineCores = info.hasNeuralEngine ? 16 : 0; - // Core split mirrors Swift getCoreDistribution(totalCores:modelId:): - // iPhone → 2P + rest E; iPad/Mac → ~40% performance (min 2); - // default → totalCores/3 performance (min 1). - const std::string& model = info.deviceModel; const int totalCores = info.coreCount; - int perfCores; - if (model.rfind("iPhone", 0) == 0) { - perfCores = 2; - } else if (model.rfind("iPad", 0) == 0 || model.rfind("Mac", 0) == 0) { - perfCores = std::max(2, totalCores * 2 / 5); + int perfCores = 0; + int effCores = 0; + if (InitBridge::shared().getCoreSplit(totalCores, perfCores, effCores)) { + info.performanceCores = perfCores; + info.efficiencyCores = effCores; } else { - perfCores = std::max(1, totalCores / 3); + // Heuristic fallback mirrors Swift + // getCoreDistribution(totalCores:modelId:): iPhone → 2P + rest E; + // iPad/Mac → ~40% performance (min 2); default → totalCores/3 (min 1). + const std::string& model = info.deviceModel; + if (model.rfind("iPhone", 0) == 0) { + perfCores = 2; + } else if (model.rfind("iPad", 0) == 0 || model.rfind("Mac", 0) == 0) { + perfCores = std::max(2, totalCores * 2 / 5); + } else { + perfCores = std::max(1, totalCores / 3); + } + perfCores = std::min(perfCores, totalCores); + info.performanceCores = perfCores; + info.efficiencyCores = totalCores - perfCores; } - perfCores = std::min(perfCores, totalCores); - info.performanceCores = perfCores; - info.efficiencyCores = totalCores - perfCores; + // Stable hardware fingerprint: SHA-256 of the invariant hardware + // tuple, distinct from the per-install persistent device UUID. + info.deviceFingerprint = ::runanywhere::sha256_hex( + info.deviceModel + "|" + info.chipName + "|" + + std::to_string(info.totalMemory) + "|" + + std::to_string(info.coreCount)); return info; }; @@ -2289,6 +2409,128 @@ bool InitBridge::isTablet() { #endif } +std::string InitBridge::getDeviceName() { +#if defined(__APPLE__) + char* value = nullptr; + if (PlatformAdapter_getDeviceName(&value) && value) { + std::string result(value); + free(value); + return result; + } + if (value) { + free(value); + } + return ""; +#elif defined(ANDROID) || defined(__ANDROID__) + return AndroidBridge::getDeviceName(); +#else + return ""; +#endif +} + +float InitBridge::getBatteryLevel() { +#if defined(__APPLE__) + return PlatformAdapter_getBatteryLevel(); +#elif defined(ANDROID) || defined(__ANDROID__) + return AndroidBridge::getBatteryLevel(); +#else + return -1.0f; +#endif +} + +std::string InitBridge::getBatteryState() { +#if defined(__APPLE__) + char* value = nullptr; + if (PlatformAdapter_getBatteryState(&value) && value) { + std::string result(value); + free(value); + return result; + } + if (value) { + free(value); + } + return ""; +#elif defined(ANDROID) || defined(__ANDROID__) + return AndroidBridge::getBatteryState(); +#else + return ""; +#endif +} + +bool InitBridge::isLowPowerMode() { +#if defined(__APPLE__) + return PlatformAdapter_isLowPowerMode(); +#elif defined(ANDROID) || defined(__ANDROID__) + return AndroidBridge::isLowPowerMode(); +#else + return false; +#endif +} + +bool InitBridge::hasNeuralEngine() { +#if defined(__APPLE__) + return getArchitecture() == "arm64"; +#elif defined(ANDROID) || defined(__ANDROID__) + return AndroidBridge::hasNPU(); +#else + return false; +#endif +} + +bool InitBridge::getCoreSplit(int totalCores, int& perfCores, int& effCores) { + if (totalCores <= 0) { + return false; + } +#if defined(__APPLE__) + const int perf = PlatformAdapter_getPerformanceCores(); + if (perf <= 0 || perf > totalCores) { + return false; + } + int eff = PlatformAdapter_getEfficiencyCores(); + if (eff < 0 || perf + eff > totalCores) { + eff = totalCores - perf; + } + perfCores = perf; + effCores = eff; + return true; +#elif defined(ANDROID) || defined(__ANDROID__) + // Group cores by cpuinfo_max_freq: cores at the highest max frequency are + // performance cores, the rest efficiency. Homogeneous CPUs report all + // cores as performance. + long maxFreq = 0; + std::vector freqs; + freqs.reserve(static_cast(totalCores)); + for (int cpu = 0; cpu < totalCores; ++cpu) { + char path[96]; + std::snprintf(path, sizeof(path), + "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu); + std::ifstream file(path); + long freq = 0; + if (!file || !(file >> freq) || freq <= 0) { + return false; + } + freqs.push_back(freq); + maxFreq = std::max(maxFreq, freq); + } + int perf = 0; + for (long freq : freqs) { + if (freq == maxFreq) { + ++perf; + } + } + if (perf <= 0 || perf > totalCores) { + return false; + } + perfCores = perf; + effCores = totalCores - perf; + return true; +#else + (void)perfCores; + (void)effCores; + return false; +#endif +} + // ============================================================================= // HTTP POST for Device Registration / Telemetry (Synchronous) // Matches Swift: CppBridge+Device.swift http_post callback diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp index b183104954..0cb91731f5 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp @@ -228,6 +228,43 @@ class InitBridge { */ bool isTablet(); + /** + * @brief Get user-visible device name (UIDevice.name / Settings device_name) + * @return Device name, or empty string when unavailable + */ + std::string getDeviceName(); + + /** + * @brief Get battery level as 0.0..1.0, or -1.0 when unknown + */ + float getBatteryLevel(); + + /** + * @brief Get battery state: "charging", "full", "unplugged", or "" when unknown + */ + std::string getBatteryState(); + + /** + * @brief Check if low power / battery saver mode is enabled + */ + bool isLowPowerMode(); + + /** + * @brief Check for a Neural Engine (Apple arm64) or NPU (Android SoC family) + */ + bool hasNeuralEngine(); + + /** + * @brief Resolve the performance/efficiency core split from the hardware. + * + * iOS reads sysctl hw.perflevel{0,1}.logicalcpu; Android groups cores by + * cpuinfo_max_freq. Outputs are only written when true is returned. + * + * @return true when the split could be determined; false → caller falls + * back to its heuristic + */ + bool getCoreSplit(int totalCores, int& perfCores, int& effCores); + // ========================================================================= // Configuration Getters (for HTTP requests in production mode) // ========================================================================= diff --git a/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.h b/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.h index 1929ad747e..d04ed86507 100644 --- a/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.h +++ b/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.h @@ -120,6 +120,40 @@ bool PlatformAdapter_getGPUFamily(char** outValue); */ bool PlatformAdapter_isTablet(void); +/** + * Get user-visible device name (UIDevice.currentDevice.name) + * @param outValue Pointer to store the result (must be freed by caller) + * @return true if successful + */ +bool PlatformAdapter_getDeviceName(char** outValue); + +/** + * Get battery level as 0.0..1.0, or -1.0 when unknown + */ +float PlatformAdapter_getBatteryLevel(void); + +/** + * Get battery state: "charging", "full", or "unplugged" + * @param outValue Pointer to store the result (must be freed by caller) + * @return true if successful; false when the state is unknown + */ +bool PlatformAdapter_getBatteryState(char** outValue); + +/** + * Check if Low Power Mode is enabled + */ +bool PlatformAdapter_isLowPowerMode(void); + +/** + * Performance (P) core count via sysctl hw.perflevel0.logicalcpu; -1 if unavailable + */ +int PlatformAdapter_getPerformanceCores(void); + +/** + * Efficiency (E) core count via sysctl hw.perflevel1.logicalcpu; -1 if unavailable + */ +int PlatformAdapter_getEfficiencyCores(void); + // ============================================================================ // App / Client Info // ============================================================================ diff --git a/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.m b/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.m index 13739a7930..640564f0c1 100644 --- a/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.m +++ b/sdk/runanywhere-react-native/packages/core/ios/PlatformAdapterBridge.m @@ -392,6 +392,7 @@ bool PlatformAdapter_getModelBaseDirectory(char** outValue) { // ============================================================================ #import +#import #import /** @@ -404,29 +405,59 @@ bool PlatformAdapter_getModelBaseDirectory(char** outValue) { } /** - * Get human-readable device model name + * Get human-readable device model name. + * Mirrors HybridRunAnywhereDeviceInfo.swift deviceModels. */ static NSString* getDeviceModelName(NSString* identifier) { - // iPhone models NSDictionary* models = @{ - // iPhone 16 series + // iPhone 17 (2025) + @"iPhone18,1": @"iPhone 17 Pro", + @"iPhone18,2": @"iPhone 17 Pro Max", + @"iPhone18,3": @"iPhone 17", + @"iPhone18,4": @"iPhone 17 Plus", + // iPhone 16 (2024) @"iPhone17,1": @"iPhone 16 Pro", @"iPhone17,2": @"iPhone 16 Pro Max", @"iPhone17,3": @"iPhone 16", @"iPhone17,4": @"iPhone 16 Plus", - // iPhone 15 series + // iPhone 15 (2023) @"iPhone16,1": @"iPhone 15 Pro", @"iPhone16,2": @"iPhone 15 Pro Max", @"iPhone15,4": @"iPhone 15", @"iPhone15,5": @"iPhone 15 Plus", - // iPhone 14 series + // iPhone 14 (2022) @"iPhone15,2": @"iPhone 14 Pro", @"iPhone15,3": @"iPhone 14 Pro Max", @"iPhone14,7": @"iPhone 14", @"iPhone14,8": @"iPhone 14 Plus", - // iPad models - @"iPad14,1": @"iPad Pro 11-inch (4th generation)", - @"iPad14,2": @"iPad Pro 12.9-inch (6th generation)", + // iPhone 13 (2021) + @"iPhone14,2": @"iPhone 13 Pro", + @"iPhone14,3": @"iPhone 13 Pro Max", + @"iPhone14,4": @"iPhone 13 mini", + @"iPhone14,5": @"iPhone 13", + // iPhone 12 (2020) + @"iPhone13,1": @"iPhone 12 mini", + @"iPhone13,2": @"iPhone 12", + @"iPhone13,3": @"iPhone 12 Pro", + @"iPhone13,4": @"iPhone 12 Pro Max", + // iPhone SE + @"iPhone14,6": @"iPhone SE (3rd gen)", + @"iPhone12,8": @"iPhone SE (2nd gen)", + // iPad Pro M4 (2024) + @"iPad16,3": @"iPad Pro 11-inch (M4)", + @"iPad16,4": @"iPad Pro 11-inch (M4)", + @"iPad16,5": @"iPad Pro 13-inch (M4)", + @"iPad16,6": @"iPad Pro 13-inch (M4)", + // iPad Pro M2 (2022) + @"iPad14,3": @"iPad Pro 11-inch (M2)", + @"iPad14,4": @"iPad Pro 11-inch (M2)", + @"iPad14,5": @"iPad Pro 12.9-inch (M2)", + @"iPad14,6": @"iPad Pro 12.9-inch (M2)", + // iPad Air + @"iPad14,8": @"iPad Air (M2)", + @"iPad14,9": @"iPad Air (M2)", + @"iPad13,16": @"iPad Air (5th gen)", + @"iPad13,17": @"iPad Air (5th gen)", // Simulator @"x86_64": @"Simulator", @"arm64": @"Simulator", @@ -437,34 +468,29 @@ bool PlatformAdapter_getModelBaseDirectory(char** outValue) { } /** - * Get chip name for device model + * Get chip name for device model. + * Mirrors HybridRunAnywhereDeviceInfo.swift getChipNameForModel(_:). */ static NSString* getChipNameForModel(NSString* identifier) { - NSDictionary* chips = @{ - // A18 Pro - @"iPhone17,1": @"A18 Pro", - @"iPhone17,2": @"A18 Pro", - // A18 - @"iPhone17,3": @"A18", - @"iPhone17,4": @"A18", - // A17 Pro - @"iPhone16,1": @"A17 Pro", - @"iPhone16,2": @"A17 Pro", - // A16 Bionic - @"iPhone15,2": @"A16 Bionic", - @"iPhone15,3": @"A16 Bionic", - @"iPhone15,4": @"A16 Bionic", - @"iPhone15,5": @"A16 Bionic", - // A15 Bionic - @"iPhone14,7": @"A15 Bionic", - @"iPhone14,8": @"A15 Bionic", - // M2 - @"iPad14,1": @"M2", - @"iPad14,2": @"M2", - }; - - NSString* chip = chips[identifier]; - return chip ?: @"Apple Silicon"; + if ([identifier hasPrefix:@"iPhone18,"]) return @"A19 Pro"; + if ([identifier hasPrefix:@"iPhone17,1"] || [identifier hasPrefix:@"iPhone17,2"]) return @"A18 Pro"; + if ([identifier hasPrefix:@"iPhone17,"]) return @"A18"; + if ([identifier hasPrefix:@"iPhone16,"]) return @"A17 Pro"; + if ([identifier hasPrefix:@"iPhone15,"]) return @"A16 Bionic"; + if ([identifier hasPrefix:@"iPhone14,"]) return @"A15 Bionic"; + if ([identifier hasPrefix:@"iPhone13,"]) return @"A14 Bionic"; + if ([identifier hasPrefix:@"iPhone12,"]) return @"A13 Bionic"; + if ([identifier hasPrefix:@"iPad16,"]) return @"M4"; + if ([identifier hasPrefix:@"iPad14,3"] || [identifier hasPrefix:@"iPad14,4"] || + [identifier hasPrefix:@"iPad14,5"] || [identifier hasPrefix:@"iPad14,6"] || + [identifier hasPrefix:@"iPad14,8"] || [identifier hasPrefix:@"iPad14,9"]) return @"M2"; + if ([identifier hasPrefix:@"iPad13,"]) return @"M1"; + +#if __arm64__ + return @"Apple Silicon"; +#else + return @"Intel"; +#endif } bool PlatformAdapter_getDeviceModel(char** outValue) { @@ -610,6 +636,120 @@ bool PlatformAdapter_isTablet(void) { } } +bool PlatformAdapter_getDeviceName(char** outValue) { + @autoreleasepool { + if (!outValue) return false; + *outValue = NULL; + + @try { + NSString* name = [[UIDevice currentDevice] name]; + if (name == nil || name.length == 0) { + return false; + } + const char* utf8Value = [name UTF8String]; + if (!utf8Value) { + return false; + } + *outValue = strdup(utf8Value); + return *outValue != NULL; + } @catch (NSException* exception) { + return false; + } + } +} + +/** + * UIDevice battery monitoring must be toggled on the main thread; battery + * reads are only meaningful afterwards. + */ +static void ensureBatteryMonitoringEnabled(void) { + if ([NSThread isMainThread]) { + [UIDevice currentDevice].batteryMonitoringEnabled = YES; + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + [UIDevice currentDevice].batteryMonitoringEnabled = YES; + }); + } +} + +float PlatformAdapter_getBatteryLevel(void) { + @autoreleasepool { + @try { + ensureBatteryMonitoringEnabled(); + float level = [UIDevice currentDevice].batteryLevel; + // -1.0 when monitoring unavailable (e.g. simulator) + return level >= 0.0f ? level : -1.0f; + } @catch (NSException* exception) { + NSLog(@"[PlatformAdapterBridge] getBatteryLevel exception: %@", exception); + return -1.0f; + } + } +} + +bool PlatformAdapter_getBatteryState(char** outValue) { + @autoreleasepool { + if (!outValue) return false; + *outValue = NULL; + + @try { + ensureBatteryMonitoringEnabled(); + UIDeviceBatteryState state = [UIDevice currentDevice].batteryState; + const char* value = NULL; + switch (state) { + case UIDeviceBatteryStateCharging: value = "charging"; break; + case UIDeviceBatteryStateFull: value = "full"; break; + case UIDeviceBatteryStateUnplugged: value = "unplugged"; break; + case UIDeviceBatteryStateUnknown: + default: break; + } + if (!value) { + return false; + } + *outValue = strdup(value); + return *outValue != NULL; + } @catch (NSException* exception) { + NSLog(@"[PlatformAdapterBridge] getBatteryState exception: %@", exception); + return false; + } + } +} + +bool PlatformAdapter_isLowPowerMode(void) { + @autoreleasepool { + @try { + return [NSProcessInfo processInfo].lowPowerModeEnabled; + } @catch (NSException* exception) { + return false; + } + } +} + +/** + * Performance (P) core count via sysctl hw.perflevel0.logicalcpu. + * Returns -1 when the kernel does not expose per-level counts. + */ +int PlatformAdapter_getPerformanceCores(void) { + uint32_t value = 0; + size_t size = sizeof(value); + if (sysctlbyname("hw.perflevel0.logicalcpu", &value, &size, NULL, 0) == 0 && value > 0) { + return (int)value; + } + return -1; +} + +/** + * Efficiency (E) core count via sysctl hw.perflevel1.logicalcpu. + * Returns -1 when the kernel does not expose per-level counts. + */ +int PlatformAdapter_getEfficiencyCores(void) { + uint32_t value = 0; + size_t size = sizeof(value); + if (sysctlbyname("hw.perflevel1.logicalcpu", &value, &size, NULL, 0) == 0) { + return (int)value; + } + return -1; +} + // ============================================================================ // App / Client Info // ============================================================================ diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Device.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Device.swift index 1443d52488..850d8f4c1c 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Device.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Device.swift @@ -121,7 +121,7 @@ extension CppBridge { if deviceInfo.hasBatteryState { outInfo.pointee.battery_state = store.dup(deviceInfo.batteryState) } - outInfo.pointee.device_fingerprint = store.dup(deviceId) + outInfo.pointee.device_fingerprint = store.dup(DeviceInfoFactory.hardwareFingerprint) } outInfo.pointee.total_memory = deviceInfo.totalMemory diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Device/Models/Domain/DeviceInfo.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Device/Models/Domain/DeviceInfo.swift index bf2a106e04..2cd6032014 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Device/Models/Domain/DeviceInfo.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Device/Models/Domain/DeviceInfo.swift @@ -6,12 +6,19 @@ // `RADeviceInfo` telemetry schema from sysctl / UIKit / ProcessInfo. // +import CryptoKit import Foundation +#if canImport(Metal) +import Metal +#endif + #if os(iOS) || os(tvOS) import UIKit #elseif os(watchOS) import WatchKit +#elseif os(macOS) +import IOKit.ps #endif /// Builds the canonical `RADeviceInfo` (generated proto) for the current @@ -36,7 +43,7 @@ public enum DeviceInfoFactory { // Get model identifier for chip/model lookup let modelId = getModelIdentifier() - let chipName = getChipName(for: modelId) + let chipSpec = getChipSpec(for: modelId) let (perfCores, effCores) = getCoreDistribution(totalCores: coreCount, modelId: modelId) // Platform-specific values @@ -48,7 +55,7 @@ public enum DeviceInfoFactory { let platform = "ios" let formFactor = device.userInterfaceIdiom == .pad ? "tablet" : "phone" - // Battery info + // Battery info (monitoring must be enabled before reading) device.isBatteryMonitoringEnabled = true let batteryLevel: Float? = device.batteryLevel >= 0 ? Float(device.batteryLevel) : nil let batteryState: String? = { @@ -64,8 +71,7 @@ public enum DeviceInfoFactory { let deviceName = Host.current().localizedName ?? "Mac" let platform = "macos" let formFactor = modelId.contains("MacBook") ? "laptop" : "desktop" - let batteryLevel: Float? = nil - let batteryState: String? = nil + let (batteryLevel, batteryState) = getMacBatteryInfo() #elseif os(tvOS) let device = UIDevice.current let deviceModel = getDeviceModelName(for: modelId) ?? device.model @@ -101,7 +107,6 @@ public enum DeviceInfoFactory { // Get available memory and clean OS version let availableMemory = getAvailableMemory() let osVersion = cleanVersion(processInfo.operatingSystemVersionString) - let hasNeuralEngine = architecture == "arm64" var info = RADeviceInfo() info.deviceModel = deviceModel @@ -110,12 +115,12 @@ public enum DeviceInfoFactory { info.osVersion = osVersion info.formFactor = formFactor info.architecture = architecture - info.chipName = chipName + info.chipName = chipSpec.name info.totalMemory = Int64(processInfo.physicalMemory) info.availableMemory = Int64(availableMemory) - info.hasNeuralEngine_p = hasNeuralEngine - info.neuralEngineCores = hasNeuralEngine ? 16 : 0 - info.gpuFamily = "apple" + info.hasNeuralEngine_p = chipSpec.hasNeuralEngine + info.neuralEngineCores = chipSpec.neuralEngineCores + info.gpuFamily = cachedGPUFamily if let batteryLevel { info.batteryLevel = batteryLevel } if let batteryState { info.batteryState = batteryState } info.isLowPowerMode = processInfo.isLowPowerModeEnabled @@ -125,6 +130,50 @@ public enum DeviceInfoFactory { return info } + /// Stable hardware fingerprint: deterministic per physical device, + /// survives reinstalls (derived only from hardware attributes). + static let hardwareFingerprint: String = { + let modelId = getModelIdentifier() + let chipName = getChipSpec(for: modelId).name + let totalMemory = ProcessInfo.processInfo.physicalMemory + let coreCount = ProcessInfo.processInfo.processorCount + let composite = "\(modelId)|\(chipName)|\(totalMemory)|\(coreCount)" + let digest = SHA256.hash(data: Data(composite.utf8)) + return digest.map { String(format: "%02x", $0) }.joined() + }() + + // MARK: - GPU Family + + private static let cachedGPUFamily: String = computeGPUFamily() + + private static func computeGPUFamily() -> String { + #if canImport(Metal) + guard let device = MTLCreateSystemDefaultDevice() else { + #if arch(arm64) + return "apple" + #else + return "intel" + #endif + } + let families: [(MTLGPUFamily, String)] = [ + (.apple9, "apple9"), (.apple8, "apple8"), (.apple7, "apple7"), + (.apple6, "apple6"), (.apple5, "apple5"), (.apple4, "apple4"), + (.apple3, "apple3"), (.apple2, "apple2"), (.apple1, "apple1") + ] + for (family, name) in families where device.supportsFamily(family) { + return name + } + #if os(macOS) + if device.supportsFamily(.mac2) { + return device.name.lowercased().contains("intel") ? "intel" : device.name.lowercased() + } + #endif + return "apple" + #else + return "apple" + #endif + } + // MARK: - System Helpers private static func getModelIdentifier() -> String { @@ -173,6 +222,42 @@ public enum DeviceInfoFactory { return totalMemory / 2 } + #if os(macOS) + private static func getMacBatteryInfo() -> (level: Float?, state: String?) { + guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(), + let sources = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue() as? [CFTypeRef] else { + return (nil, nil) + } + for source in sources { + guard let description = IOPSGetPowerSourceDescription(snapshot, source)? + .takeUnretainedValue() as? [String: Any], + description[kIOPSTypeKey] as? String == kIOPSInternalBatteryType else { + continue + } + var level: Float? + if let current = description[kIOPSCurrentCapacityKey] as? Int, + let maxCapacity = description[kIOPSMaxCapacityKey] as? Int, + maxCapacity > 0 { + level = Float(current) / Float(maxCapacity) + } + let isCharging = description[kIOPSIsChargingKey] as? Bool ?? false + let state: String? + if isCharging { + state = "charging" + } else if let level, level >= 1.0 { + state = "full" + } else if level != nil { + state = "unplugged" + } else { + state = nil + } + return (level, state) + } + // Desktop Mac: no internal battery is legitimate + return (nil, nil) + } + #endif + // MARK: - Device Model Lookup (minimal, common devices only) private static func getDeviceModelName(for identifier: String) -> String? { @@ -200,37 +285,97 @@ public enum DeviceInfoFactory { return models[identifier] } - private static func getChipName(for identifier: String) -> String { - // Map model prefix to chip - if identifier.hasPrefix("iPhone18,") { return "A19 Pro" } - if identifier.hasPrefix("iPhone17,1") || identifier.hasPrefix("iPhone17,2") { return "A18 Pro" } - if identifier.hasPrefix("iPhone17,") { return "A18" } - if identifier.hasPrefix("iPhone16,") { return "A17 Pro" } - if identifier.hasPrefix("iPhone15,2") || identifier.hasPrefix("iPhone15,3") { return "A16 Bionic" } - if identifier.hasPrefix("iPhone15,") { return "A16 Bionic" } - if identifier.hasPrefix("iPhone14,") { return "A15 Bionic" } - if identifier.hasPrefix("iPad16,") || identifier.hasPrefix("Mac16,") { return "M4" } - if identifier.hasPrefix("iPad15,") || identifier.hasPrefix("Mac15,") { return "M3" } - if identifier.hasPrefix("Mac14,") { return "M2" } + // MARK: - Chip Lookup + + private struct ChipSpec { + let name: String + let neuralEngineCores: Int32 + let hasNeuralEngine: Bool + } + + private static func getChipSpec(for identifier: String) -> ChipSpec { + // Ordered prefix table: most specific entries first. + let table: [(prefix: String, name: String, aneCores: Int32)] = [ + // iPhone + ("iPhone18,", "A19 Pro", 16), + ("iPhone17,1", "A18 Pro", 16), ("iPhone17,2", "A18 Pro", 16), + ("iPhone17,", "A18", 16), + ("iPhone16,", "A17 Pro", 16), + ("iPhone15,", "A16 Bionic", 16), + ("iPhone14,", "A15 Bionic", 16), + ("iPhone13,", "A14 Bionic", 16), + ("iPhone12,", "A13 Bionic", 8), + ("iPhone11,", "A12 Bionic", 8), + ("iPhone10,", "A11 Bionic", 2), + // iPad + ("iPad16,", "M4", 16), + ("iPad15,", "M3", 16), + ("iPad14,1", "A15 Bionic", 16), ("iPad14,2", "A15 Bionic", 16), + ("iPad14,", "M2", 16), + ("iPad13,1", "A14 Bionic", 16), ("iPad13,2", "A14 Bionic", 16), + ("iPad13,", "M1", 16), + // Mac + ("Mac16,", "M4", 16), + ("Mac15,14", "M3 Ultra", 32), + ("Mac15,", "M3", 16), + ("Mac14,8", "M2 Ultra", 32), ("Mac14,14", "M2 Ultra", 32), + ("Mac14,13", "M2 Max", 16), + ("Mac14,", "M2", 16), + ("Mac13,2", "M1 Ultra", 32), + ("Mac13,1", "M1 Max", 16), + ("MacBookPro18,", "M1 Pro/Max", 16), + ("MacBookPro17,1", "M1", 16), + ("MacBookAir10,1", "M1", 16), + ("Macmini9,1", "M1", 16), + ("iMac21,", "M1", 16), + // Vision Pro + ("RealityDevice14,", "M2", 16) + ] + for entry in table where identifier.hasPrefix(entry.prefix) { + return ChipSpec( + name: entry.name, + neuralEngineCores: entry.aneCores, + hasNeuralEngine: true + ) + } #if arch(arm64) - return "Apple Silicon" + // Unknown Apple Silicon: report the raw identifier so telemetry stays + // informative; ANE presence is a safe assumption, core count is not. + let name = identifier.isEmpty ? "Apple Silicon" : identifier + return ChipSpec(name: name, neuralEngineCores: 0, hasNeuralEngine: true) #else - return "Intel" + return ChipSpec(name: "Intel", neuralEngineCores: 0, hasNeuralEngine: false) #endif } + // MARK: - Core Distribution + + private static func sysctlInt32(_ name: String) -> Int? { + var value: Int32 = 0 + var size = MemoryLayout.size + guard sysctlbyname(name, &value, &size, nil, 0) == 0, value > 0 else { return nil } + return Int(value) + } + private static func getCoreDistribution(totalCores: Int, modelId: String) -> (perf: Int, eff: Int) { - // iPhone: typically 2P + 4E = 6 cores + // Real values from the kernel (iOS 15+ / macOS 12+) + if let perf = sysctlInt32("hw.perflevel0.logicalcpu") { + let eff = sysctlInt32("hw.perflevel1.logicalcpu") ?? 0 + if perf + eff == totalCores { + return (perf, eff) + } + return (min(perf, totalCores), max(0, totalCores - min(perf, totalCores))) + } + + // Heuristic fallback if modelId.hasPrefix("iPhone") { return (2, totalCores - 2) } - // iPad/Mac M-series: typically ~40% performance cores if modelId.hasPrefix("iPad") || modelId.hasPrefix("Mac") { let perf = max(2, totalCores * 2 / 5) return (perf, totalCores - perf) } - // Default split return (max(1, totalCores / 3), totalCores - max(1, totalCores / 3)) } } diff --git a/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts b/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts index 4d706e37c8..3144c1fd4a 100644 --- a/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts +++ b/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts @@ -42,13 +42,25 @@ interface BrowserNavigator extends Navigator { userAgentData?: { mobile?: boolean; platform?: string; + brands?: ReadonlyArray<{ brand?: string; version?: string }>; }; + getBattery?: () => Promise<{ level?: number; charging?: boolean }>; } -interface BrowserPerformance extends Performance { - memory?: { - usedJSHeapSize?: number; - jsHeapSizeLimit?: number; +interface WebGPUAdapterInfoLike { + vendor?: string; + architecture?: string; + description?: string; +} + +interface WebGPUAdapterLike { + info?: WebGPUAdapterInfoLike; + requestAdapterInfo?: () => Promise; +} + +interface NavigatorWithWebGPU { + gpu?: { + requestAdapter: () => Promise; }; } @@ -159,11 +171,39 @@ interface DeviceProfile { architecture: 'unknown'; chipName: string; totalMemory: number; - availableMemory: number; - gpuFamily: string | null; + /** Browsers expose no real free-RAM API; 0 means unknown rather than a JS-heap guess. */ + availableMemory: 0; + gpuFamily: string; + batteryLevel: number; + batteryState: string | null; + deviceFingerprint: string | null; coreCount: number; } +/** + * Hardware facts that only async browser APIs can produce (WebGPU adapter, + * Battery Status, WebCrypto digest). Pre-fetched once per page so the + * synchronous native `get_device_info` callback can consume cached values. + */ +interface HardwareSnapshot { + gpuFamily: string; + chipName: string; + batteryLevel: number; + batteryState: string | null; + fingerprint: string; +} + +const DEFAULT_HARDWARE_SNAPSHOT: HardwareSnapshot = { + gpuFamily: 'unknown', + chipName: 'unknown', + batteryLevel: -1, + batteryState: null, + fingerprint: '', +}; + +let hardwareSnapshot: HardwareSnapshot = DEFAULT_HARDWARE_SNAPSHOT; +let hardwareSnapshotPrefetch: Promise | null = null; + interface ResolvedControlPlaneConfiguration { baseURL: string; apiKey: string; @@ -233,35 +273,181 @@ function browserOSVersion(userAgent: string, platform: string): string { return (normalizedPlatform || 'unknown').slice(0, MAX_OS_VERSION_LENGTH); } +function browserOSName(userAgent: string, uaDataPlatform: string): string { + const normalized = uaDataPlatform.toLowerCase(); + if (normalized.startsWith('win')) return 'Windows'; + if (normalized === 'macos' || normalized.startsWith('mac')) return 'macOS'; + if (normalized === 'android') return 'Android'; + if (normalized === 'ios') return 'iOS'; + if (normalized === 'chromeos' || normalized === 'chrome os') return 'ChromeOS'; + if (normalized === 'linux') return 'Linux'; + if (/Windows/i.test(userAgent)) return 'Windows'; + if (/Android/i.test(userAgent)) return 'Android'; + if (/iPhone|iPad|iPod/i.test(userAgent)) return 'iOS'; + if (/Mac OS X|Macintosh/i.test(userAgent)) return 'macOS'; + if (/CrOS/i.test(userAgent)) return 'ChromeOS'; + if (/Linux/i.test(userAgent)) return 'Linux'; + return uaDataPlatform.trim() || 'Web'; +} + +function browserName(nav: BrowserNavigator): string { + const brands = nav.userAgentData?.brands ?? []; + const realBrands = brands + .map((entry) => entry.brand?.trim() ?? '') + .filter((brand) => brand.length > 0 && !/not.?a.?brand/i.test(brand)); + const brand = realBrands.find((name) => !/^chromium$/i.test(name)) ?? realBrands[0]; + if (brand) return brand; + const ua = nav.userAgent; + if (/Firefox\//i.test(ua)) return 'Firefox'; + if (/Edg(?:e|A|iOS)?\//i.test(ua)) return 'Edge'; + if (/OPR\//i.test(ua)) return 'Opera'; + if (/Chrome\//i.test(ua)) return 'Chrome'; + if (/Safari\//i.test(ua)) return 'Safari'; + return 'Browser'; +} + +/** GPU renderer string via the WebGL debug extension (sync, cheap, cacheable). */ +function webglRendererString(): string { + try { + const canvas = document.createElement('canvas'); + const gl = (canvas.getContext('webgl') ?? canvas.getContext('experimental-webgl')) as + | WebGLRenderingContext + | null; + if (!gl) return ''; + const debugInfo = gl.getExtension('WEBGL_debug_renderer_info') as + | { UNMASKED_RENDERER_WEBGL: number } + | null; + if (!debugInfo) return ''; + const renderer: unknown = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); + return typeof renderer === 'string' ? renderer.trim() : ''; + } catch { + return ''; + } +} + +function normalizeGPUFamily(renderer: string): string { + const value = renderer.toLowerCase(); + if (!value) return ''; + if (value.includes('apple')) return 'apple'; + if (value.includes('adreno')) return 'adreno'; + if (value.includes('mali')) return 'mali'; + if (value.includes('nvidia') || value.includes('geforce') || value.includes('quadro')) { + return 'nvidia'; + } + if (value.includes('amd') || value.includes('radeon')) return 'amd'; + if (value.includes('intel')) return 'intel'; + return ''; +} + +async function webgpuAdapterInfo(): Promise<{ family: string; description: string }> { + try { + const gpu = (navigator as unknown as NavigatorWithWebGPU).gpu; + if (!gpu) return { family: '', description: '' }; + const adapter = await gpu.requestAdapter(); + if (!adapter) return { family: '', description: '' }; + let info = adapter.info; + if (!info && typeof adapter.requestAdapterInfo === 'function') { + info = await adapter.requestAdapterInfo(); + } + return { + family: info?.architecture?.trim() || info?.vendor?.trim() || '', + description: info?.description?.trim() ?? '', + }; + } catch { + return { family: '', description: '' }; + } +} + +async function batteryStatus(): Promise<{ level: number; state: string | null }> { + try { + const nav = navigator as BrowserNavigator; + if (typeof nav.getBattery !== 'function') return { level: -1, state: null }; + const battery = await nav.getBattery(); + const rawLevel = battery.level; + if (typeof rawLevel !== 'number' || !Number.isFinite(rawLevel)) { + return { level: -1, state: null }; + } + const level = Math.min(1, Math.max(0, rawLevel)); + const charging = battery.charging === true; + const state = charging ? (level === 1 ? 'full' : 'charging') : 'unplugged'; + return { level, state }; + } catch { + return { level: -1, state: null }; + } +} + +/** Stable composite hardware fingerprint (SHA-256 hex over coarse hardware facts). */ +async function computeDeviceFingerprint(renderer: string): Promise { + try { + const nav = navigator as BrowserNavigator; + const material = [ + nav.userAgentData?.platform?.trim() || nav.platform?.trim() || '', + String(nav.hardwareConcurrency ?? 0), + String(nav.deviceMemory ?? 0), + renderer, + ].join('|'); + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(material)); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join(''); + } catch { + return ''; + } +} + +async function collectHardwareSnapshot(): Promise { + const renderer = webglRendererString(); + const [gpu, battery, fingerprint] = await Promise.all([ + webgpuAdapterInfo(), + batteryStatus(), + computeDeviceFingerprint(renderer), + ]); + return { + gpuFamily: gpu.family || normalizeGPUFamily(renderer) || 'unknown', + chipName: renderer || gpu.description || 'unknown', + batteryLevel: battery.level, + batteryState: battery.state, + fingerprint, + }; +} + +function prefetchHardwareSnapshot(): Promise { + hardwareSnapshotPrefetch ??= collectHardwareSnapshot() + .then((snapshot) => { + hardwareSnapshot = snapshot; + }) + .catch(() => { + // Defaults stay in place; every collector already degrades per-field. + }); + return hardwareSnapshotPrefetch; +} + function browserDeviceProfile(): DeviceProfile { const nav = navigator as BrowserNavigator; - const perf = performance as BrowserPerformance; - const platform = nav.userAgentData?.platform?.trim() - || nav.platform?.trim() - || 'Web Browser'; + const hardware = hardwareSnapshot; + const osName = browserOSName( + nav.userAgent, + nav.userAgentData?.platform?.trim() ?? nav.platform?.trim() ?? '', + ); const coreCount = Math.max(1, Math.trunc(nav.hardwareConcurrency || 1)); const totalMemory = Math.max( BYTES_PER_GIB, Math.trunc((nav.deviceMemory || DEFAULT_DEVICE_MEMORY_GIB) * BYTES_PER_GIB), ); - const heapLimit = perf.memory?.jsHeapSizeLimit; - const heapUsed = perf.memory?.usedJSHeapSize ?? 0; - const availableMemory = typeof heapLimit === 'number' && Number.isFinite(heapLimit) - ? Math.min(totalMemory, Math.max(0, Math.trunc(heapLimit - heapUsed))) - : totalMemory; const mobile = nav.userAgentData?.mobile ?? /Android|iPhone|iPad|Mobile/i.test(nav.userAgent); return { - deviceModel: platform, - deviceName: document.title.trim() || 'RunAnywhere Web', - osVersion: browserOSVersion(nav.userAgent, platform), + deviceModel: `${osName} ${mobile ? 'Mobile' : 'Desktop'}`, + deviceName: `${browserName(nav)} on ${osName}`, + osVersion: browserOSVersion(nav.userAgent, osName), formFactor: mobile ? 'phone' : 'desktop', architecture: 'unknown', - chipName: platform, + chipName: hardware.chipName, totalMemory, - availableMemory, - gpuFamily: 'gpu' in nav ? 'webgpu' : null, + availableMemory: 0, + gpuFamily: hardware.gpuFamily, + batteryLevel: hardware.batteryLevel, + batteryState: hardware.batteryState, + deviceFingerprint: hardware.fingerprint || null, coreCount, }; } @@ -313,6 +499,9 @@ export class DeviceRegistrationAdapter { module: DeviceRegistrationModule, configuration: DeviceRegistrationConfiguration, ): DeviceRegistrationAdapter { + // Async hardware facts (GPU adapter, battery, fingerprint digest) are cached + // before native registration retries so the sync callback can use them. + void prefetchHardwareSnapshot(); const adapter = new DeviceRegistrationAdapter(module, configuration); adapter.register(); DeviceRegistrationAdapter.installedAdapters.set(module, adapter); @@ -490,8 +679,12 @@ export class DeviceRegistrationAdapter { this.module.setValue(outInfoPtr + this.deviceInfoLayout.hasNeuralEngine, 0, 'i32'); this.module.setValue(outInfoPtr + this.deviceInfoLayout.neuralEngineCores, 0, 'i32'); writeString(this.deviceInfoLayout.gpuFamily, profile.gpuFamily); - this.module.setValue(outInfoPtr + this.deviceInfoLayout.batteryLevel, -1, 'double'); - this.module.setValue(outInfoPtr + this.deviceInfoLayout.batteryState, 0, '*'); + this.module.setValue( + outInfoPtr + this.deviceInfoLayout.batteryLevel, + profile.batteryLevel, + 'double', + ); + writeString(this.deviceInfoLayout.batteryState, profile.batteryState); this.module.setValue(outInfoPtr + this.deviceInfoLayout.isLowPowerMode, 0, 'i32'); this.module.setValue(outInfoPtr + this.deviceInfoLayout.coreCount, profile.coreCount, 'i32'); this.module.setValue( @@ -500,11 +693,15 @@ export class DeviceRegistrationAdapter { 'i32', ); this.module.setValue(outInfoPtr + this.deviceInfoLayout.efficiencyCores, 0, 'i32'); - this.module.setValue( - outInfoPtr + this.deviceInfoLayout.deviceFingerprint, - deviceIdPtr, - '*', - ); + if (profile.deviceFingerprint) { + writeString(this.deviceInfoLayout.deviceFingerprint, profile.deviceFingerprint); + } else { + this.module.setValue( + outInfoPtr + this.deviceInfoLayout.deviceFingerprint, + deviceIdPtr, + '*', + ); + } } private readDeviceIdPointer(): number { From 58f6141b339e45c084eb7f72e606ae060b5f9d5a Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Wed, 22 Jul 2026 01:16:31 +0530 Subject: [PATCH 22/44] examples: flutter/rn use production env with build-time dev credentials (dart-define / .env), matching android --- .../runanywhereai/RunAnywhereApplication.kt | 12 ++++++------ .../plugins/GeneratedPluginRegistrant.java | 5 +++++ .../ios/Runner/GeneratedPluginRegistrant.m | 7 +++++++ .../lib/app/runanywhere_ai_app.dart | 19 ++++++++----------- examples/react-native/RunAnywhereAI/App.tsx | 18 ++++++++---------- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt index d65bca17c0..6f07e43940 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt @@ -107,12 +107,12 @@ class RunAnywhereApplication : Application() { // resolves the baked staging backend URL and sends unauthenticated // telemetry (PUBLIC-org ingestion). Restore the config-driven // selection below to go back to production/development behavior. - val environment = SDKEnvironment.SDK_ENVIRONMENT_STAGING - // val environment = if (hasBackendConfig) { - // SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION - // } else { - // SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - // } + //val environment = SDKEnvironment.SDK_ENVIRONMENT_STAGING + val environment = if (hasBackendConfig) { + SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION + } else { + SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT + } RunAnywhere.initialize( context = this@RunAnywhereApplication, apiKey = BuildConfig.RUNANYWHERE_API_KEY.takeIf { diff --git a/examples/flutter/RunAnywhereAI/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/examples/flutter/RunAnywhereAI/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 85acd14260..cf27ce3b45 100644 --- a/examples/flutter/RunAnywhereAI/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/examples/flutter/RunAnywhereAI/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -20,6 +20,11 @@ public static void registerWith(@NonNull FlutterEngine flutterEngine) { } catch (Exception e) { Log.e(TAG, "Error registering plugin audioplayers_android, xyz.luan.audioplayers.AudioplayersPlugin", e); } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.battery.BatteryPlusPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin battery_plus, dev.fluttercommunity.plus.battery.BatteryPlusPlugin", e); + } try { flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.device_info.DeviceInfoPlusPlugin()); } catch (Exception e) { diff --git a/examples/flutter/RunAnywhereAI/ios/Runner/GeneratedPluginRegistrant.m b/examples/flutter/RunAnywhereAI/ios/Runner/GeneratedPluginRegistrant.m index b706c02918..90d5de3907 100644 --- a/examples/flutter/RunAnywhereAI/ios/Runner/GeneratedPluginRegistrant.m +++ b/examples/flutter/RunAnywhereAI/ios/Runner/GeneratedPluginRegistrant.m @@ -12,6 +12,12 @@ @import audioplayers_darwin; #endif +#if __has_include() +#import +#else +@import battery_plus; +#endif + #if __has_include() #import #else @@ -100,6 +106,7 @@ @implementation GeneratedPluginRegistrant + (void)registerWithRegistry:(NSObject*)registry { [AudioplayersDarwinPlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioplayersDarwinPlugin"]]; + [FPPBatteryPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPBatteryPlusPlugin"]]; [FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]]; [FilePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FilePickerPlugin"]]; [FlutterSecureStorageDarwinPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterSecureStorageDarwinPlugin"]]; diff --git a/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart b/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart index 6c8a8847ad..8febe32fd8 100644 --- a/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart +++ b/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart @@ -75,21 +75,18 @@ class _RunAnywhereAIAppState extends State { await RunAnywhere.initialize( apiKey: customApiKey, baseURL: normalizedURL, - // Staging (not Production) so the custom base URL is honored AND - // local logging stays on — Production sets enableLocalLogging:false, - // hiding all SDK/telemetry logs. Development would ignore baseURL. - environment: SDKEnvironment.SDK_ENVIRONMENT_STAGING, + // Production + explicit creds — same proven path as the Android + // example (custom URL honored, full bearer auth + registration). + environment: SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION, ); - debugPrint('✅ SDK initialized with CUSTOM configuration (staging)'); + debugPrint('✅ SDK initialized with CUSTOM configuration (production)'); } else { - // Staging test build: keyless staging — no API key, no URL; the SDK - // resolves the baked staging backend URL and sends unauthenticated - // telemetry (PUBLIC-org ingestion). Restore `RunAnywhere.initialize()` - // to go back to development behavior. + // No credentials supplied: development without a key — telemetry and + // registration stay local-only until credentials are provided. await RunAnywhere.initialize( - environment: SDKEnvironment.SDK_ENVIRONMENT_STAGING, + environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, ); - debugPrint('✅ SDK initialized in STAGING mode (keyless)'); + debugPrint('✅ SDK initialized in DEVELOPMENT mode (keyless)'); } // Re-apply the persisted HuggingFace token (Settings screen) so private diff --git a/examples/react-native/RunAnywhereAI/App.tsx b/examples/react-native/RunAnywhereAI/App.tsx index 9a9053c491..8424de4e9f 100644 --- a/examples/react-native/RunAnywhereAI/App.tsx +++ b/examples/react-native/RunAnywhereAI/App.tsx @@ -224,27 +224,25 @@ const App: React.FC = () => { if (configuration) { console.log('[App] Found backend configuration'); - // Staging (not Production) so the custom base URL is honored AND - // local logging stays on — Production sets enableLocalLogging:false, - // hiding all SDK/telemetry logs. Development would ignore baseURL. + // Production + explicit creds — same proven path as the Android + // example (custom URL honored, full bearer auth + registration). await RunAnywhere.initialize({ apiKey: configuration.apiKey, baseURL: configuration.baseURL, - environment: SDKEnvironment.SDK_ENVIRONMENT_STAGING, + environment: SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION, }); console.log( - '[App] SDK initialized with backend configuration (staging)' + '[App] SDK initialized with backend configuration (production)' ); } else { - // Staging test build: keyless staging — no API key, no URL; the SDK - // resolves the baked staging backend URL and sends unauthenticated - // telemetry (PUBLIC-org ingestion). Restore DEVELOPMENT to go back. + // No .env credentials: development without a key — telemetry and + // registration stay local-only until credentials are provided. await RunAnywhere.initialize({ apiKey: '', baseURL: '', - environment: SDKEnvironment.SDK_ENVIRONMENT_STAGING, + environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, }); - console.log('[App] SDK initialized in STAGING mode (keyless)'); + console.log('[App] SDK initialized in DEVELOPMENT mode (keyless)'); } await registerAll(backendState); From 283629f52c9231249d883a92f3cbb0786b3d0853 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 12:51:41 -0700 Subject: [PATCH 23/44] sdks: drop baked Supabase creds + build-token from commons & iOS (keep neutral STAGING_BASE_URL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Released binaries no longer embed the Supabase project URL / anon key or the obsolete build token. Only a neutral backend base URL (STAGING_BASE_URL) is baked, so the SDK reaches the backend solely through a vanity host we control and can re-point without shipping a new SDK. - commons: reduce development_config template to STAGING_BASE_URL only; remove supabase/build-token getters, is_available, has_supabase, has_build_token from header + exports; drop the SUPABASE/BUILD_TOKEN CMake injection + JNI shims. - commons: http_setup_applicable_for_state() no longer has a Supabase dev branch — every environment reaches the backend through a usable base URL (keyless ok). - release.yml: drop SUPABASE_URL / SUPABASE_ANON_KEY / BUILD_TOKEN secrets. - iOS: trim CppBridge.DevConfig to the shared usability checks; unify Phase-2 HTTP setup to the effective base URL for all envs; remove dev-Supabase telemetry gate. Commons core build-verified: strings librac_commons.a shows no supabase/anon_key/railway strings; generated config carries only the placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .github/workflows/release.yml | 17 ++- sdk/runanywhere-commons/CMakeLists.txt | 24 ++-- .../exports/RACommons.exports | 8 +- .../infrastructure/network/rac_dev_config.h | 56 +-------- .../network/development_config.cpp.template | 116 ++---------------- .../src/jni/runanywhere_commons_jni.cpp | 36 ------ .../src/lifecycle/sdk_init.cpp | 10 +- .../CRACommons/include/rac_dev_config.h | 61 ++------- .../Extensions/CppBridge+Environment.swift | 73 +---------- .../Extensions/CppBridge+Telemetry.swift | 6 - .../RunAnywhere/Public/RunAnywhere.swift | 27 ++-- 11 files changed, 61 insertions(+), 373 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45f97c0a05..6a2a31c8ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,15 +38,14 @@ permissions: env: RELEASE_VERSION: ${{ github.event.inputs.version || github.ref_name }} - # Development-analytics config baked into rac_commons at build time. Kept in CI - # secrets (never in the public source tree); the commons CMake substitutes them - # into the generated development_config.cpp when present (else a credential-free - # stub). See sdk/runanywhere-commons/CMakeLists.txt (DEVELOPMENT CONFIG SOURCE). - SUPABASE_URL: ${{ secrets.SUPABASE_URL }} - SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - # Staging backend base URL baked the same way — lets environment=staging run - # keyless with no explicit URL (see rac_dev_config_get_staging_base_url). + # Neutral backend base URL baked into rac_commons at build time — lets + # environment=staging run keyless with no explicit URL (see + # rac_dev_config_get_staging_base_url). Kept in a CI secret (never in the public + # source tree); the commons CMake substitutes it into the generated + # development_config.cpp when present (else a placeholder stub). This is a + # neutral vanity host (e.g. api.runanywhere.ai) — no credentials, project refs, + # or tokens are ever embedded. See sdk/runanywhere-commons/CMakeLists.txt + # (DEVELOPMENT CONFIG SOURCE). STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} jobs: diff --git a/sdk/runanywhere-commons/CMakeLists.txt b/sdk/runanywhere-commons/CMakeLists.txt index 8571dcb6da..a2de169d75 100644 --- a/sdk/runanywhere-commons/CMakeLists.txt +++ b/sdk/runanywhere-commons/CMakeLists.txt @@ -675,25 +675,19 @@ else() message(FATAL_ERROR "Missing credential-free development config template: ${DEV_CONFIG_TEMPLATE}") endif() set(RAC_DEV_CONFIG_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/generated/development_config.cpp") - # The tracked template carries only "YOUR_*" placeholders (never real - # credentials). Release/CI builds substitute the real values from the - # environment (GitHub Actions secrets SUPABASE_URL / SUPABASE_ANON_KEY / - # BUILD_TOKEN) into the generated source — so credentials live in CI secrets, - # never in the public source tree. When the env is unset (normal local build) - # the placeholders pass through and the dev config stays the credential-free - # stub (usability checks treat "YOUR_*" as unavailable). + # The tracked template carries only the "YOUR_STAGING_BASE_URL" placeholder + # (never a real host). Release/CI builds substitute the neutral backend base + # URL from the environment (GitHub Actions secret STAGING_BASE_URL) into the + # generated source. When the env is unset (normal local build) the placeholder + # passes through and the dev config stays a stub (usability checks treat + # "YOUR_*" as unavailable). No credentials, project refs, or tokens are ever + # baked — only this neutral base URL. file(READ "${DEV_CONFIG_TEMPLATE}" _rac_dev_cfg) - if(NOT "$ENV{SUPABASE_URL}" STREQUAL "") - string(REPLACE "YOUR_SUPABASE_PROJECT_URL" "$ENV{SUPABASE_URL}" _rac_dev_cfg "${_rac_dev_cfg}") - string(REPLACE "YOUR_SUPABASE_ANON_KEY" "$ENV{SUPABASE_ANON_KEY}" _rac_dev_cfg "${_rac_dev_cfg}") - string(REPLACE "YOUR_BUILD_TOKEN" "$ENV{BUILD_TOKEN}" _rac_dev_cfg "${_rac_dev_cfg}") - message(STATUS "Injected development config from environment (CI secrets)") - else() - message(STATUS "Using credential-free development config stub") - endif() if(NOT "$ENV{STAGING_BASE_URL}" STREQUAL "") string(REPLACE "YOUR_STAGING_BASE_URL" "$ENV{STAGING_BASE_URL}" _rac_dev_cfg "${_rac_dev_cfg}") message(STATUS "Injected staging base URL from environment (CI secret)") + else() + message(STATUS "Using placeholder staging base URL (no baked backend URL)") endif() file(WRITE "${RAC_DEV_CONFIG_SOURCE}" "${_rac_dev_cfg}") endif() diff --git a/sdk/runanywhere-commons/exports/RACommons.exports b/sdk/runanywhere-commons/exports/RACommons.exports index 764cbbcacc..ee88a03343 100644 --- a/sdk/runanywhere-commons/exports/RACommons.exports +++ b/sdk/runanywhere-commons/exports/RACommons.exports @@ -495,13 +495,8 @@ _rac_validate_base_url _rac_validate_config _rac_validation_error_message -# Dev-only config (Supabase URL/key, build token) — reached +# Dev config (staging base URL + shared usability checks) — reached # through CppBridge+Environment.swift. -_rac_dev_config_get_build_token -_rac_dev_config_get_supabase_key -_rac_dev_config_get_supabase_url -_rac_dev_config_has_build_token -_rac_dev_config_is_available _rac_dev_config_is_usable_credential _rac_dev_config_is_usable_http_url @@ -835,7 +830,6 @@ _rac_cpu_runtime_get_provider_session _rac_cpu_runtime_register_provider _rac_cpu_runtime_unregister_provider _rac_detect_archive_type -_rac_dev_config_has_supabase _rac_device_registration_endpoint _rac_device_registration_to_json _rac_diffusion_model_registry_cleanup diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h index 2e8254fac3..44224b8c89 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h @@ -2,21 +2,15 @@ * @file rac_dev_config.h * @brief Development mode configuration API * - * Provides access to development mode configuration values. Normal builds use - * the credential-free tracked template. Developers may explicitly opt in to - * the ignored development_config.cpp with RAC_INCLUDE_LOCAL_DEV_CONFIG=ON. - * - * This allows: - * - Cross-platform sharing of explicitly enabled local development config - * - Git-ignored credentials with a credential-free build default - * - Consistent development environment across SDKs + * Provides access to the baked staging backend base URL. Normal builds use the + * tracked template (placeholder only). Developers may explicitly opt in to the + * ignored development_config.cpp with RAC_INCLUDE_LOCAL_DEV_CONFIG=ON. * * Security Model: * - development_config.cpp is in .gitignore (not committed to main branch) * - Normal, CI, and release builds never compile the ignored local file - * - Local values require RAC_INCLUDE_LOCAL_DEV_CONFIG=ON and must never be packaged - * - Values are used only when the SDK is in .development mode - * - Backend validates build token via POST /api/v1/devices/register/dev + * - Only a neutral backend base URL is ever baked — no credentials, project + * refs, or tokens. The SDK reaches the backend solely through this base URL. */ #ifndef RAC_DEV_CONFIG_H @@ -34,30 +28,6 @@ extern "C" { // Development Configuration API // ============================================================================= -/** - * @brief Check if development config is available - * @return true if development config is properly configured - */ -RAC_API bool rac_dev_config_is_available(void); - -/** - * @brief Get Supabase project URL for development mode - * @return URL string (static, do not free) - */ -RAC_API const char* rac_dev_config_get_supabase_url(void); - -/** - * @brief Get Supabase anon key for development mode - * @return API key string (static, do not free) - */ -RAC_API const char* rac_dev_config_get_supabase_key(void); - -/** - * @brief Get build token for development mode - * @return Build token string (static, do not free) - */ -RAC_API const char* rac_dev_config_get_build_token(void); - /** * @brief Get the baked staging backend base URL * @@ -69,22 +39,6 @@ RAC_API const char* rac_dev_config_get_build_token(void); */ RAC_API const char* rac_dev_config_get_staging_base_url(void); -// ============================================================================= -// Convenience Functions -// ============================================================================= - -/** - * @brief Check if Supabase config is valid - * @return true if URL and key are non-empty - */ -RAC_API bool rac_dev_config_has_supabase(void); - -/** - * @brief Check if build token is valid - * @return true if build token is non-empty - */ -RAC_API bool rac_dev_config_has_build_token(void); - // ============================================================================= // Usability Checks (canonical, shared by all SDKs) // ============================================================================= diff --git a/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template b/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template index d276c3fc62..0ede1bac93 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template +++ b/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template @@ -4,104 +4,28 @@ * * SETUP INSTRUCTIONS: * 1. Copy this file to development_config.cpp - * 2. Fill in your development credentials - * 3. development_config.cpp is git-ignored, so your secrets won't be committed + * 2. Fill in your development values + * 3. development_config.cpp is git-ignored, so your config won't be committed * - * For RunAnywhere team members: - * - Get credentials from the team's secure credential storage - * - Contact team lead for access to development credentials + * Only the staging backend base URL is baked here (so environment=staging can + * run keyless with no explicit URL). CI substitutes STAGING_BASE_URL from a + * secret; open-source builds keep the placeholder and pass a base URL + * explicitly. No credentials, project refs, or tokens are embedded — the SDK + * reaches the backend only through this neutral base URL. */ -#include -#include -#include -#include - -#include "rac/core/rac_logger.h" #include "rac/infrastructure/network/rac_dev_config.h" // ============================================================================= -// Configuration Values - FILL IN YOUR CREDENTIALS BELOW +// Configuration Values // ============================================================================= namespace { -// Supabase project URL for development device analytics -// Get this from: https://supabase.com/dashboard → Your Project → Settings → API -constexpr const char* SUPABASE_URL = "YOUR_SUPABASE_PROJECT_URL"; - -// Supabase anon/public API key -// Get this from: https://supabase.com/dashboard → Your Project → Settings → API → anon key -constexpr const char* SUPABASE_ANON_KEY = "YOUR_SUPABASE_ANON_KEY"; - -// Development mode build token -// Get this from your team's credential storage, or use a debug token for local dev -constexpr const char* BUILD_TOKEN = "YOUR_BUILD_TOKEN"; - // Staging backend base URL — baked into team builds so environment=staging // needs no explicit URL. Leave the placeholder to require an explicit URL. constexpr const char* STAGING_BASE_URL = "YOUR_STAGING_BASE_URL"; -std::string trim(const char* value) { - if (!value) { - return {}; - } - - std::string out(value); - auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; - out.erase(out.begin(), std::find_if(out.begin(), out.end(), [&](char c) { - return !is_space(static_cast(c)); - })); - out.erase(std::find_if(out.rbegin(), out.rend(), [&](char c) { - return !is_space(static_cast(c)); - }).base(), - out.end()); - return out; -} - -std::string lowercase(std::string value) { - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - return value; -} - -bool looks_like_placeholder(const char* value) { - std::string normalized = lowercase(trim(value)); - if (normalized.empty()) { - return true; - } - - return normalized.find("your_") != std::string::npos || - normalized.find("") == std::string::npos; -} - } // anonymous namespace // ============================================================================= @@ -110,32 +34,8 @@ bool is_usable_http_url(const char* value) { extern "C" { -bool rac_dev_config_is_available(void) { - return rac_dev_config_has_supabase(); -} - -const char* rac_dev_config_get_supabase_url(void) { - return SUPABASE_URL; -} - -const char* rac_dev_config_get_supabase_key(void) { - return SUPABASE_ANON_KEY; -} - -const char* rac_dev_config_get_build_token(void) { - return BUILD_TOKEN; -} - const char* rac_dev_config_get_staging_base_url(void) { return STAGING_BASE_URL; } -bool rac_dev_config_has_supabase(void) { - return is_usable_http_url(SUPABASE_URL) && !looks_like_placeholder(SUPABASE_ANON_KEY); -} - -bool rac_dev_config_has_build_token(void) { - return !looks_like_placeholder(BUILD_TOKEN); -} - } // extern "C" diff --git a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp index 21600ac1f7..842f6a20a4 100644 --- a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp +++ b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp @@ -3493,42 +3493,6 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racAnalyticsEventEmitVa // Mirrors Swift SDK's CppBridge+Environment.swift DevConfig // ============================================================================= -JNIEXPORT jboolean JNICALL -Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racDevConfigIsAvailable(JNIEnv* env, - jclass clazz) { - return rac_dev_config_is_available() ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jstring JNICALL -Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racDevConfigGetSupabaseUrl(JNIEnv* env, - jclass clazz) { - const char* url = rac_dev_config_get_supabase_url(); - if (url == nullptr || strlen(url) == 0) { - return nullptr; - } - return env->NewStringUTF(url); -} - -JNIEXPORT jstring JNICALL -Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racDevConfigGetSupabaseKey(JNIEnv* env, - jclass clazz) { - const char* key = rac_dev_config_get_supabase_key(); - if (key == nullptr || strlen(key) == 0) { - return nullptr; - } - return env->NewStringUTF(key); -} - -JNIEXPORT jstring JNICALL -Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racDevConfigGetBuildToken(JNIEnv* env, - jclass clazz) { - const char* token = rac_dev_config_get_build_token(); - if (token == nullptr || strlen(token) == 0) { - return nullptr; - } - return env->NewStringUTF(token); -} - JNIEXPORT jboolean JNICALL Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racDevConfigIsUsableCredential( JNIEnv* env, jclass clazz, jstring value) { diff --git a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp index d7f85e0dae..ea3544d7da 100644 --- a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp +++ b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp @@ -110,17 +110,15 @@ bool environment_requires_external_config(rac_environment_t env) { bool http_setup_applicable_for_state() { const rac_environment_t env = rac_state_get_environment(); - if (!environment_requires_external_config(env)) { - return rac_dev_config_is_usable_http_url(rac_dev_config_get_supabase_url()) && - rac_dev_config_is_usable_credential(rac_dev_config_get_supabase_key()); - } - const char* api_key = rac_state_get_api_key(); const char* base_url = rac_state_get_base_url(); + // Every environment reaches the backend through a usable base URL — there is + // no direct-to-datastore path anymore. if (!rac_dev_config_is_usable_http_url(base_url)) { return false; } - // Keyless staging is a valid HTTP setup (unauthenticated public ingestion) + // Keyless (dev / keyless-staging) is a valid HTTP setup: unauthenticated + // public-org ingestion. Auth-required environments need a usable API key. return rac_dev_config_is_usable_credential(api_key) || !rac_env_auth_expected(env, api_key); } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h b/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h index aadd5f7602..2f4b744e98 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h @@ -2,21 +2,15 @@ * @file rac_dev_config.h * @brief Development mode configuration API * - * Provides access to development mode configuration values. Normal builds use - * the credential-free tracked template. Developers may explicitly opt in to - * the ignored development_config.cpp with RAC_INCLUDE_LOCAL_DEV_CONFIG=ON. - * - * This allows: - * - Cross-platform sharing of explicitly enabled local development config - * - Git-ignored credentials with a credential-free build default - * - Consistent development environment across SDKs + * Provides access to the baked staging backend base URL and the canonical + * usability checks. Only a neutral backend base URL is ever baked — no + * credentials, project refs, or tokens. The SDK reaches the backend solely + * through this base URL. * * Security Model: * - development_config.cpp is in .gitignore (not committed to main branch) * - Normal, CI, and release builds never compile the ignored local file - * - Local values require RAC_INCLUDE_LOCAL_DEV_CONFIG=ON and must never be packaged - * - Values are used only when the SDK is in .development mode - * - Backend validates build token via POST /api/v1/devices/register/dev + * - Local opt-in requires RAC_INCLUDE_LOCAL_DEV_CONFIG=ON and must never be packaged */ #ifndef RAC_DEV_CONFIG_H @@ -35,44 +29,15 @@ extern "C" { // ============================================================================= /** - * @brief Check if development config is available - * @return true if development config is properly configured - */ -RAC_API bool rac_dev_config_is_available(void); - -/** - * @brief Get Supabase project URL for development mode - * @return URL string (static, do not free) - */ -RAC_API const char* rac_dev_config_get_supabase_url(void); - -/** - * @brief Get Supabase anon key for development mode - * @return API key string (static, do not free) - */ -RAC_API const char* rac_dev_config_get_supabase_key(void); - -/** - * @brief Get build token for development mode - * @return Build token string (static, do not free) - */ -RAC_API const char* rac_dev_config_get_build_token(void); - -// ============================================================================= -// Convenience Functions -// ============================================================================= - -/** - * @brief Check if Supabase config is valid - * @return true if URL and key are non-empty - */ -RAC_API bool rac_dev_config_has_supabase(void); - -/** - * @brief Check if build token is valid - * @return true if build token is non-empty + * @brief Get the baked staging backend base URL + * + * Team builds bake the staging URL via the git-ignored development_config.cpp + * so callers can init with environment=staging and nothing else. Open-source + * builds keep the placeholder and must pass a base URL explicitly. + * + * @return URL string or placeholder (static, do not free) */ -RAC_API bool rac_dev_config_has_build_token(void); +RAC_API const char* rac_dev_config_get_staging_base_url(void); // ============================================================================= // Usability Checks (canonical, shared by all SDKs) diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift index 7739466406..5a429e7107 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift @@ -68,78 +68,11 @@ extension CppBridge { extension CppBridge { /// Development configuration bridge - /// Wraps C++ rac_dev_config.h functions - /// Used for development mode with Supabase backend + /// Wraps the canonical commons usability checks from rac_dev_config.h so + /// every SDK agrees on the placeholder/URL rules. The backend is reached + /// only through the effective base URL — no credentials are baked in. public enum DevConfig { - /// Check if development config is available - public static var isAvailable: Bool { - rac_dev_config_is_available() - } - - /// Get Supabase URL for development mode - public static var supabaseURL: String? { - guard isAvailable else { return nil } - guard let ptr = rac_dev_config_get_supabase_url() else { return nil } - return String(cString: ptr) - } - - /// Get Supabase API key for development mode - public static var supabaseKey: String? { - guard isAvailable else { return nil } - guard let ptr = rac_dev_config_get_supabase_key() else { return nil } - return String(cString: ptr) - } - - /// True when the development Supabase config is present and not a template placeholder. - public static var hasUsableSupabaseConfig: Bool { - guard let urlString = supabaseURL, - let apiKey = supabaseKey, - isUsableHTTPURL(urlString), - isUsableCredential(apiKey) else { - return false - } - return true - } - - /// True when development device registration has all required values. - public static var hasUsableDevelopmentRegistrationConfig: Bool { - hasUsableSupabaseConfig && hasUsableBuildToken - } - - /// Get build token for development mode - public static var buildToken: String? { - guard rac_dev_config_has_build_token() else { return nil } - guard let ptr = rac_dev_config_get_build_token() else { return nil } - let token = String(cString: ptr) - return isUsableCredential(token) ? token : nil - } - - /// True when the development build token is present and not a placeholder. - public static var hasUsableBuildToken: Bool { - buildToken != nil - } - - /// Configure CppBridge.HTTP for development mode using C++ config - /// - Returns: true if configured successfully, false if config not available - @discardableResult - public static func configureHTTP() async -> Bool { - guard hasUsableSupabaseConfig, - let rawURLString = supabaseURL, - let rawAPIKey = supabaseKey else { - return false - } - - let urlString = rawURLString.trimmingCharacters(in: .whitespacesAndNewlines) - let apiKey = rawAPIKey.trimmingCharacters(in: .whitespacesAndNewlines) - - guard let url = URL(string: urlString) else { - return false - } - await CppBridge.HTTP.shared.configure(baseURL: url, apiKey: apiKey) - return true - } - /// Whether a baked-in credential is usable: non-empty and not a /// scaffolding placeholder. Delegates to the canonical commons rule /// (`rac_dev_config_is_usable_credential`) so every SDK agrees instead diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swift index 0f94b18642..b5aab41204 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swift @@ -304,12 +304,6 @@ private func telemetryHttpCallback( private func performTelemetryHTTP(path: String, json: String, requiresAuth: Bool) async { let logger = SDKLogger(category: "CppBridge.Telemetry") - let environment = CppBridge.Telemetry.environment - - if environment == .development && !CppBridge.DevConfig.hasUsableSupabaseConfig { - logger.debug("Skipping telemetry/device registration: no usable config") - return - } let hasUsableConfiguration = await CppBridge.HTTP.hasUsableConfiguration guard hasUsableConfiguration else { diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift index 39c20313b1..2ec17928b4 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift @@ -403,7 +403,7 @@ public enum RunAnywhere { /// Swift retains only platform-service callback registration. private static func _performServicesInitialization() async throws { let snapshot = state.withLock { ($0.initParams, $0.currentEnvironment) } - guard let params = snapshot.0, let environment = snapshot.1 else { + guard let params = snapshot.0, snapshot.1 != nil else { throw SDKException(code: .notInitialized, message: "SDK not initialized", category: .internal) } @@ -412,22 +412,16 @@ public enum RunAnywhere { // Step 1: configure the Swift HTTP adapter used by callback-based // platform services. Auth and control-plane orchestration stay in C++. if await !CppBridge.HTTP.shared.isConfigured { - if environment == .development { - if await CppBridge.DevConfig.configureHTTP() { - logger.debug("HTTP adapter configured from C++ development config") - } else { - logger.debug("HTTP adapter disabled: no usable development config") - } + // Effective config from commons state, for every environment: staging + // resolves the baked keyless base URL, dev/prod use whatever the app + // passed. There is no direct-to-datastore path — the backend is always + // reached through this base URL. Auth stays in C++. + let effectiveURLString = CppBridge.State.baseURL ?? params.baseURL.absoluteString + if CppBridge.DevConfig.isUsableHTTPURL(effectiveURLString), + let effectiveURL = URL(string: effectiveURLString) { + await CppBridge.HTTP.shared.configure(baseURL: effectiveURL, apiKey: params.apiKey) } else { - // Effective config from commons state: staging overrides - // whatever the app passed (baked URL, keyless). - let effectiveURLString = CppBridge.State.baseURL ?? params.baseURL.absoluteString - if CppBridge.DevConfig.isUsableHTTPURL(effectiveURLString), - let effectiveURL = URL(string: effectiveURLString) { - await CppBridge.HTTP.shared.configure(baseURL: effectiveURL, apiKey: params.apiKey) - } else { - logger.debug("HTTP adapter disabled: no usable external config") - } + logger.debug("HTTP adapter disabled: no usable external config") } } @@ -438,7 +432,6 @@ public enum RunAnywhere { // Step 3 (C++): auth, device registration, model assignments, // telemetry flush, and downloaded-model discovery. let phase2Result = try CppBridge.SdkInit.phase2( - buildToken: environment == .development ? CppBridge.DevConfig.buildToken : nil, forceRefreshAssignments: false, flushTelemetry: true, discoverDownloadedModels: true, From abbd3356ba2739eec70d5892281b5ba7827d90ff Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 12:58:48 -0700 Subject: [PATCH 24/44] react-native: drop baked Supabase creds + build-token reads (mirror commons/iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors commit 283629f5. RN native no longer reads the removed commons dev-config accessors — dev/staging/prod all reach the backend through the effective base URL from commons state (keyless staging → PUBLIC org). Build token is no longer sourced from dev config (Phase-2 proto field sent empty). - TelemetryBridge.cpp / InitBridge.cpp: remove the DEVELOPMENT Supabase branches reading rac_dev_config_get_supabase_url/_key and rac_dev_config_get_build_token; fall through to the effective-base-URL path. - InitBridge.hpp: rename the now-generic httpPostSync token param (supabaseKey → apiKey). - HybridRunAnywhereCore+Common.hpp: fix stale rac_dev_config include comment. - RunAnywhere.ts: phase2Request.buildToken hard-set to '' (no dev-config read). Inert Supabase upsert HTTP plumbing left intact (matches iOS). grep: no remaining references to the removed rac_dev_config_* accessors under packages/core/cpp. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../core/cpp/HybridRunAnywhereCore+Common.hpp | 2 +- .../packages/core/cpp/bridges/InitBridge.cpp | 49 +++++++------------ .../packages/core/cpp/bridges/InitBridge.hpp | 4 +- .../core/cpp/bridges/TelemetryBridge.cpp | 27 ++-------- .../packages/core/src/Public/RunAnywhere.ts | 4 +- 5 files changed, 28 insertions(+), 58 deletions(-) diff --git a/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Common.hpp b/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Common.hpp index 81b4cb5068..ae4bfd9a6b 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Common.hpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Common.hpp @@ -9,7 +9,7 @@ #include "HybridRunAnywhereCore.hpp" // RACommons headers -#include "rac_dev_config.h" // For rac_dev_config_get_build_token +#include "rac_dev_config.h" // Dev-config usability checks (staging base URL only) // Core bridges - aligned with actual RACommons API #include "bridges/InitBridge.hpp" diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp index e3041d697c..19918c87f5 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp @@ -1826,13 +1826,10 @@ InitBridge::initialize(rac_environment_t environment, const std::string &apiKey, LOGI("SDK Phase 1 proto initialized"); } + // The effective build token is whatever the app passed (or empty). There is + // no baked dev build token anymore — the backend is reached solely through + // the effective base URL. std::string effectiveBuildToken = buildToken; - if (effectiveBuildToken.empty() && environment == RAC_ENV_DEVELOPMENT) { - const char *devBuildToken = rac_dev_config_get_build_token(); - if (devBuildToken && config::isUsableSecret(devBuildToken)) { - effectiveBuildToken = devBuildToken; - } - } phase2RequestBytes_ = makePhase2RequestBytes( effectiveBuildToken, forceRefreshAssignments, flushTelemetry, discoverDownloadedModels, rescanLocalModels); @@ -1938,30 +1935,18 @@ rac_result_t InitBridge::registerDeviceCallbacks() { ) -> std::tuple { (void)requiresAuth; - rac_environment_t env = InitBridge::shared().getEnvironment(); - std::string baseURL; - std::string token; - - if (env == RAC_ENV_DEVELOPMENT) { - auto supabaseConfig = config::makeEndpointConfig( - rac_dev_config_get_supabase_url() ? rac_dev_config_get_supabase_url() : "", - rac_dev_config_get_supabase_key() ? rac_dev_config_get_supabase_key() : ""); - if (!supabaseConfig.usable) { - LOGI("Skipping development device registration: no usable config"); - return {true, 204, "{}", ""}; - } - baseURL = supabaseConfig.baseURL; - token = supabaseConfig.token; - } else { - baseURL = config::trim(InitBridge::shared().getBaseURL()); - std::string accessToken = AuthBridge::shared().getAccessToken(); - token = config::isUsableSecret(accessToken) - ? accessToken - : config::trim(InitBridge::shared().getApiKey()); - if (!config::isUsableHttpUrl(baseURL) || !config::isUsableSecret(token)) { - LOGI("Skipping device registration: no usable external config"); - return {true, 204, "{}", ""}; - } + // Effective config from commons state, for every environment: staging + // resolves the baked keyless base URL, dev/prod use whatever the app + // passed. There is no direct-to-datastore path — the backend is always + // reached through this base URL. + std::string baseURL = config::trim(InitBridge::shared().getBaseURL()); + std::string accessToken = AuthBridge::shared().getAccessToken(); + std::string token = config::isUsableSecret(accessToken) + ? accessToken + : config::trim(InitBridge::shared().getApiKey()); + if (!config::isUsableHttpUrl(baseURL) || !config::isUsableSecret(token)) { + LOGI("Skipping device registration: no usable external config"); + return {true, 204, "{}", ""}; } std::string fullURL = config::appendEndpointPath(baseURL, endpoint); @@ -2297,10 +2282,10 @@ bool InitBridge::isTablet() { std::tuple InitBridge::httpPostSync( const std::string& url, const std::string& jsonBody, - const std::string& supabaseKey + const std::string& apiKey ) { LOGI("httpPostSync via rac_http_client_* starting"); - auto result = postJsonViaRacHttpClient(url, jsonBody, supabaseKey); + auto result = postJsonViaRacHttpClient(url, jsonBody, apiKey); LOGI("httpPostSync result: success=%d statusCode=%d", std::get<0>(result), std::get<1>(result)); return result; diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp index b183104954..f3dbb5dab4 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp @@ -273,12 +273,12 @@ class InitBridge { * * @param url Full URL to POST to * @param jsonBody JSON body string - * @param supabaseKey Supabase API key (for dev mode, empty for prod) + * @param apiKey Authorization token for the request (empty for keyless) * @return tuple */ std::tuple httpPostSync(const std::string &url, const std::string &jsonBody, - const std::string &supabaseKey); + const std::string &apiKey); private: InitBridge() = default; diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp index 495b25c283..e04a5f2029 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp @@ -298,28 +298,11 @@ static void telemetryHttpCallback(void *userData, const char *endpoint, std::string baseURL; std::string apiKey; - if (env == RAC_ENV_DEVELOPMENT) { - // Development: Use Supabase from C++ dev config (development_config.cpp) - // NO FALLBACK - credentials must come from C++ config only - auto supabaseConfig = config::makeEndpointConfig( - rac_dev_config_get_supabase_url() ? rac_dev_config_get_supabase_url() - : "", - rac_dev_config_get_supabase_key() ? rac_dev_config_get_supabase_key() - : ""); - - if (!supabaseConfig.usable) { - LOGI("Skipping telemetry/device registration: no usable config"); - rac_telemetry_manager_http_complete(manager, RAC_TRUE, "{}", nullptr); - return; - } - - baseURL = supabaseConfig.baseURL; - apiKey = supabaseConfig.token; - LOGD("Telemetry using configured development Supabase endpoint"); - } else { - // Production/Staging: read the effective URL from commons state — - // staging overrides whatever the app passed (baked URL, keyless) — - // falling back to the SDK-initialization value. + { + // Effective config from commons state, for every environment: staging + // overrides whatever the app passed (baked URL, keyless), dev/prod use the + // effective URL falling back to the SDK-initialization value. There is no + // direct-to-datastore path — the backend is always reached through this URL. const char *stateURL = rac_state_get_base_url(); baseURL = (stateURL != nullptr && stateURL[0] != '\0') ? config::trim(stateURL) diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts index d4561488a2..305b9e28eb 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts @@ -279,7 +279,9 @@ export const RunAnywhere = { const phase2Request: SdkInitPhase2RequestMessage = SdkInitPhase2Request.create(); - phase2Request.buildToken = options.buildToken?.trim() ?? ''; + // The baked dev build token is gone; the backend is reached solely + // through the effective base URL. Keep the proto field, always empty. + phase2Request.buildToken = ''; phase2Request.forceRefreshAssignments = options.forceRefreshAssignments ?? false; phase2Request.flushTelemetry = options.flushTelemetry ?? true; From 7a903c2c8a41c9b052fb656cacff61e199140e31 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 13:01:24 -0700 Subject: [PATCH 25/44] web: drop baked Supabase creds + build-token wasm exports (mirror commons/iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors commit 283629f5. Removes the WASM dev-config export wrappers that read the deleted commons accessors, and reroutes device registration / telemetry to the effective base URL from commons state (keyless staging → PUBLIC org). - wasm_exports.cpp + wasm/CMakeLists.txt: remove rac_wasm_dev_config_is_available / _get_supabase_url / _get_supabase_key / _get_build_token wrappers + their EXPORTED_FUNCTIONS entries. - DeviceRegistrationAdapter.ts: currentControlPlaneConfiguration() resolves solely from the configured base URL + api key; dropped the dev-config module methods. - RunAnywhere.ts: phase2 buildToken sent empty; no dev-config read. - tests: device-registration upsert case driven via an installed base URL + api key instead of dev-config stubs. Inert Supabase upsert plumbing left intact (matches iOS). typecheck (tsc --noEmit): PASS. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../src/Adapters/DeviceRegistrationAdapter.ts | 29 ++++++++----------- .../packages/core/src/Public/RunAnywhere.ts | 8 ++--- .../DeviceRegistrationAdapter.test.ts | 15 +++++----- sdk/runanywhere-web/wasm/CMakeLists.txt | 6 ---- sdk/runanywhere-web/wasm/src/wasm_exports.cpp | 27 ----------------- 5 files changed, 23 insertions(+), 62 deletions(-) diff --git a/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts b/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts index 4d706e37c8..841d252f91 100644 --- a/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts +++ b/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts @@ -59,9 +59,6 @@ export interface DeviceRegistrationModule extends EmscriptenRunanywhereModule { _rac_state_is_device_registered?(): number; _rac_state_set_device_registered?(registered: number): void; _rac_auth_get_access_token?(): number; - _rac_wasm_dev_config_get_supabase_url?(): number; - _rac_wasm_dev_config_get_supabase_key?(): number; - _rac_wasm_dev_config_is_available?(): number; _rac_wasm_sizeof_device_callbacks?(): number; _rac_wasm_offsetof_device_callbacks_get_device_info?(): number; @@ -658,21 +655,19 @@ export class DeviceRegistrationAdapter { return this.readNativeString(this.module._rac_auth_get_access_token); } - /** Resolve URL + credential atomically so embedded secrets never cross origins. */ + /** + * Resolve URL + credential atomically so embedded secrets never cross origins. + * + * No released SDK bakes or reads Supabase credentials or a build token: the + * backend is reached only through the effective base URL supplied by commons + * state (keyless staging attributes to a PUBLIC org). The dev/keyless path + * therefore falls through to the same effective-base-URL configuration as any + * other environment — there is no separate dev-config credential branch. + */ private currentControlPlaneConfiguration(): ResolvedControlPlaneConfiguration | null { - if (this.configuredBaseURL || this.configuredApiKey) { - return this.configuredBaseURL && this.configuredApiKey - ? { baseURL: this.configuredBaseURL, apiKey: this.configuredApiKey } - : null; - } - try { - if (this.module._rac_wasm_dev_config_is_available?.() !== 1) return null; - const baseURL = this.readNativeString(this.module._rac_wasm_dev_config_get_supabase_url); - const apiKey = this.readNativeString(this.module._rac_wasm_dev_config_get_supabase_key); - return baseURL && apiKey ? { baseURL, apiKey } : null; - } catch { - return null; - } + return this.configuredBaseURL && this.configuredApiKey + ? { baseURL: this.configuredBaseURL, apiKey: this.configuredApiKey } + : null; } private async prepareHTTPRequest(options: { diff --git a/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts index 0df5851444..437a9e0a83 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts @@ -1322,9 +1322,9 @@ export const RunAnywhere = { if (typeof module._rac_sdk_init_phase2_proto === 'function') { const environment = _initOptions?.environment ?? SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; const bytes = SdkInitPhase2Request.encode({ - buildToken: environment === SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - ? (_initOptions?.buildToken ?? '') - : '', + // No released SDK bakes or forwards a build token; the backend is + // reached only through the effective base URL (keyless staging). + buildToken: '', forceRefreshAssignments: false, flushTelemetry: true, discoverDownloadedModels: true, @@ -1340,7 +1340,7 @@ export const RunAnywhere = { await completePendingDeviceRegistration( module, environment, - _initOptions?.buildToken ?? '', + '', lifecycleGeneration, ); if (lifecycleGeneration !== _lifecycleGeneration) return; diff --git a/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts b/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts index 77bd483da3..dc7a28f287 100644 --- a/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts +++ b/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts @@ -75,7 +75,7 @@ interface FakeModuleHandle { setNativeRegistered(value: boolean): void; } -function createFakeModule(options: { devConfigAvailable?: boolean } = {}): FakeModuleHandle { +function createFakeModule(): FakeModuleHandle { const memory = new ArrayBuffer(1 << 20); const heap = new Uint8Array(memory); const view = new DataView(memory); @@ -108,8 +108,6 @@ function createFakeModule(options: { devConfigAvailable?: boolean } = {}): FakeM }; const deviceIdPtr = writeString('web-device-id'); const accessTokenPtr = writeString('test-access-token'); - const devURLPtr = writeString('https://development.invalid'); - const devKeyPtr = writeString('test-development-key'); const moduleShape: Partial = { HEAPU8: heap, @@ -157,9 +155,6 @@ function createFakeModule(options: { devConfigAvailable?: boolean } = {}): FakeM _rac_state_is_device_registered: () => nativeRegistered ? 1 : 0, _rac_state_set_device_registered(value: number): void { nativeRegistered = value !== 0; }, _rac_auth_get_access_token: () => accessTokenPtr, - _rac_wasm_dev_config_is_available: () => options.devConfigAvailable ? 1 : 0, - _rac_wasm_dev_config_get_supabase_url: () => devURLPtr, - _rac_wasm_dev_config_get_supabase_key: () => devKeyPtr, _rac_wasm_sizeof_device_callbacks: () => CALLBACK.size, _rac_wasm_offsetof_device_callbacks_get_device_info: () => CALLBACK.getInfo, @@ -379,8 +374,12 @@ describe('DeviceRegistrationAdapter', () => { fetchStub.mockClear(); fetchStub.mockResolvedValueOnce({ ok: true, status: 204 } as Response); - const dev = createFakeModule({ devConfigAvailable: true }); + // Dev/keyless upsert now flows through the effective base URL + API key + // supplied by commons state (no baked Supabase dev-config credentials). + const dev = createFakeModule(); const devAdapter = DeviceRegistrationAdapter.install(dev.module, { + baseURL: 'https://development.invalid', + apiKey: 'test-development-key', environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, sdkVersion: '0.19.13', }); @@ -423,7 +422,7 @@ describe('DeviceRegistrationAdapter', () => { { baseURL: 'https://attacker.invalid' }, { apiKey: 'configured-key-without-origin' }, ]) { - const handle = createFakeModule({ devConfigAvailable: true }); + const handle = createFakeModule(); const adapter = DeviceRegistrationAdapter.install(handle.module, { ...configuration, environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, diff --git a/sdk/runanywhere-web/wasm/CMakeLists.txt b/sdk/runanywhere-web/wasm/CMakeLists.txt index 703770fb67..f44c9748e0 100644 --- a/sdk/runanywhere-web/wasm/CMakeLists.txt +++ b/sdk/runanywhere-web/wasm/CMakeLists.txt @@ -970,12 +970,6 @@ set(RAC_EXPORTED_FUNCTIONS_BASE "_rac_wasm_sizeof_embeddings_options" "_rac_wasm_sizeof_embeddings_result" - # Dev config WASM wrappers - "_rac_wasm_dev_config_is_available" - "_rac_wasm_dev_config_get_supabase_url" - "_rac_wasm_dev_config_get_supabase_key" - "_rac_wasm_dev_config_get_build_token" - # Emscripten runtime helpers "_malloc" "_free" diff --git a/sdk/runanywhere-web/wasm/src/wasm_exports.cpp b/sdk/runanywhere-web/wasm/src/wasm_exports.cpp index 398540ab04..de8ecc5ec3 100644 --- a/sdk/runanywhere-web/wasm/src/wasm_exports.cpp +++ b/sdk/runanywhere-web/wasm/src/wasm_exports.cpp @@ -937,33 +937,6 @@ EMSCRIPTEN_KEEPALIVE int rac_wasm_offsetof_proto_buffer_error_message(void) { return (int)offsetof(rac_proto_buffer_t, error_message); } -// ============================================================================= -// DEV CONFIG WRAPPERS -// -// Expose development configuration values (Supabase URL/key, build token) -// so that the TypeScript HTTP layer can use them for dev-mode telemetry. -// ============================================================================= - -EMSCRIPTEN_KEEPALIVE -int rac_wasm_dev_config_is_available(void) { - return rac_dev_config_is_available() ? 1 : 0; -} - -EMSCRIPTEN_KEEPALIVE -const char *rac_wasm_dev_config_get_supabase_url(void) { - return rac_dev_config_get_supabase_url(); -} - -EMSCRIPTEN_KEEPALIVE -const char *rac_wasm_dev_config_get_supabase_key(void) { - return rac_dev_config_get_supabase_key(); -} - -EMSCRIPTEN_KEEPALIVE -const char *rac_wasm_dev_config_get_build_token(void) { - return rac_dev_config_get_build_token(); -} - // ============================================================================= // FILE MANAGER WRAPPERS (clearCache / cleanTempFiles) // From 7589c992b18f995724d37e7e096edd291e94e5f6 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 13:04:25 -0700 Subject: [PATCH 26/44] kotlin: drop baked Supabase creds + build-token bridge (mirror commons/iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors commit 283629f5. Removes the dead JNI externs and CppBridgeDevConfig members for Supabase URL/key + build token; unifies Phase-2 HTTP setup so every environment configures from the effective base URL (racStateGetBaseUrl ?? telemetry base) instead of a dev-only Supabase branch. Keyless staging → PUBLIC org. - RunAnywhereBridge.kt: remove racDevConfigIsAvailable/GetSupabaseUrl/GetSupabaseKey/ GetBuildToken external funs (natives deleted in commons). - CppBridgeEnvironment.kt: trim CppBridgeDevConfig to isUsableCredential/isUsableHTTPURL. - CppBridge.kt: unified effective-base-URL HTTP setup for all environments. - CppBridgeTelemetry.kt: remove dev-Supabase telemetry gate (+ unused env param). - RunAnywhere.kt: phase2 buildToken = null. Inert Supabase upsert plumbing left intact (matches iOS). grep clean; Gradle compile pending (deps/native not bootstrapped in this env). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../sdk/foundation/bridge/CppBridge.kt | 37 ++++----- .../bridge/extensions/CppBridgeEnvironment.kt | 76 +------------------ .../bridge/extensions/CppBridgeTelemetry.kt | 11 +-- .../sdk/native/bridge/RunAnywhereBridge.kt | 28 ------- .../com/runanywhere/sdk/public/RunAnywhere.kt | 8 +- 5 files changed, 25 insertions(+), 135 deletions(-) diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt index 36b4178f8f..5ecf06e570 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt @@ -388,27 +388,28 @@ object CppBridge { // platform services. Auth and control-plane orchestration are // driven by rac_sdk_init_phase2_proto through the registered // OkHttp transport. + // + // Effective config from commons state, for every environment: + // staging resolves the baked keyless base URL, dev/prod use whatever + // the app passed. There is no direct-to-datastore (dev Supabase) + // path — the backend is always reached through this base URL, and + // auth stays in C++. Mirrors Swift's `_performServicesInitialization` + // Step 1. if (!HTTPClientAdapter.isConfigured) { + val effectiveBaseUrl = + RunAnywhereBridge.racStateGetBaseUrl() + ?.takeIf { it.isNotEmpty() } + ?: CppBridgeTelemetry.getBaseUrl() + val apiKey = CppBridgeTelemetry.getApiKey() val configured = - if (_environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { - CppBridgeDevConfig.configureHTTP() + if (!effectiveBaseUrl.isNullOrEmpty() && + CppBridgeDevConfig.isUsableHTTPURL(effectiveBaseUrl) + ) { + HTTPClientAdapter.configure(effectiveBaseUrl, apiKey) + true } else { - // Read the effective config from commons state: staging - // overrides whatever the app passed (baked URL, keyless). - val baseUrl = RunAnywhereBridge.racStateGetBaseUrl() - ?.takeIf { it.isNotEmpty() } - ?: CppBridgeTelemetry.getBaseUrl() - val apiKey = CppBridgeTelemetry.getApiKey() - if (!baseUrl.isNullOrEmpty()) { - HTTPClientAdapter.configure(baseUrl, apiKey) - true - } else { - logger.warn( - "HTTP adapter NOT configured: baseUrl present=${!baseUrl.isNullOrEmpty()}, " + - "apiKey present=${!apiKey.isNullOrEmpty()}", - ) - false - } + logger.debug("HTTP adapter disabled: no usable external config") + false } logger.info( "Phase 2 HTTP adapter configuration: configured=$configured " + diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt index 7966020b0c..2e0c73358e 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt @@ -24,7 +24,6 @@ package com.runanywhere.sdk.foundation.bridge.extensions -import com.runanywhere.sdk.foundation.bridge.HTTPClientAdapter import com.runanywhere.sdk.native.bridge.RunAnywhereBridge import com.runanywhere.sdk.public.configuration.SDKEnvironment import com.runanywhere.sdk.public.configuration.cEnvironment @@ -146,10 +145,9 @@ object CppBridgeEnvironment { /** * Development configuration bridge. * - * Wraps the four `rac_dev_config_*` accessors that ship with the - * commons library and are populated by `development_config.cpp` (the - * Supabase + build-token bundle used in dev mode). Mirrors Swift's - * `CppBridge.DevConfig` enum namespace. + * Wraps the two `rac_dev_config_*` usability helpers that ship with the + * commons library (`is_usable_credential` / `is_usable_http_url`). + * Mirrors Swift's `CppBridge.DevConfig` enum namespace. * * Thread safety: every accessor delegates to the native side which is * read-only after build time; no Kotlin-side locking is required. @@ -158,74 +156,6 @@ object CppBridgeDevConfig { private val placeholderPattern: Regex = Regex("YOUR_| Date: Tue, 21 Jul 2026 13:04:25 -0700 Subject: [PATCH 27/44] flutter: drop baked Supabase creds + build-token bridge (mirror commons/iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors commit 283629f5. Removes the DartBridgeDevConfig Supabase/build-token getters (FFI lookups on the deleted commons symbols) and the dev-only Supabase adapter config; every environment now configures the HTTP adapter from the effective base URL. Keyless staging → PUBLIC org. - dart_bridge_environment.dart: trim DartBridgeDevConfig to isUsableCredential/isUsableHttpUrl. - runanywhere.dart: unified effective-base-URL Phase-2 setup; buildToken no longer sourced. - sdk_environment.dart: remove SupabaseConfig class + supabaseConfig getter. - http_client_adapter.dart: remove configureDev + _supabase* fields/headers. - dart_bridge.dart: buildToken sent empty. Inert Supabase upsert plumbing (_maybeAppendSupabaseUpsert) left intact (matches iOS). grep clean; dart analyze pending (deps not bootstrapped in this env). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../lib/adapters/http_client_adapter.dart | 90 +++++-------------- .../runanywhere/lib/native/dart_bridge.dart | 3 +- .../lib/native/dart_bridge_environment.dart | 89 +----------------- .../public/configuration/sdk_environment.dart | 47 +--------- .../runanywhere/lib/public/runanywhere.dart | 22 ++--- 5 files changed, 37 insertions(+), 214 deletions(-) diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart index 4616528652..f058b1ba92 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart @@ -25,7 +25,6 @@ import 'package:ffi/ffi.dart'; import 'package:runanywhere/core/native/rac_native.dart'; import 'package:runanywhere/foundation/constants/sdk_constants.dart'; import 'package:runanywhere/foundation/logging/sdk_logger.dart'; -import 'package:runanywhere/native/dart_bridge_environment.dart'; import 'package:runanywhere/native/types/basic_types.dart'; import 'package:runanywhere/public/configuration/sdk_environment.dart'; @@ -95,7 +94,6 @@ class _HttpAdapterSnapshot { required this.environment, required this.accessToken, required this.timeoutMs, - required this.supabaseKey, required this.tokenResolver, required this.refreshTokenCallback, }); @@ -106,7 +104,6 @@ class _HttpAdapterSnapshot { final SDKEnvironment environment; final String? accessToken; final int timeoutMs; - final String supabaseKey; final Future Function({required bool requiresAuth})? tokenResolver; final Future Function()? refreshTokenCallback; } @@ -140,9 +137,6 @@ class HTTPClientAdapter { String? _accessToken; int _timeoutMs = defaultTimeoutMs; - // Development (Supabase) overrides. - String _supabaseURL = ''; - String _supabaseKey = ''; int _configurationGeneration = 0; // Per-request token resolver (injected by auth bridge to avoid a @@ -168,48 +162,19 @@ class HTTPClientAdapter { _logger.info('Configured for ${environment.name} environment'); } - void configureDev({ - required String supabaseURL, - required String supabaseKey, - }) { - _configurationGeneration++; - if (!DartBridgeDevConfig.isUsableHttpUrl(supabaseURL) || - !DartBridgeDevConfig.isUsableCredential(supabaseKey)) { - _supabaseURL = ''; - _supabaseKey = ''; - _logger.warning('Dev Supabase config ignored: missing or placeholder'); - return; - } - - _supabaseURL = supabaseURL; - _supabaseKey = supabaseKey; - _logger.info('Dev mode configured with Supabase'); - } - void setToken(String? token) { _accessToken = token; } String? get accessToken => _accessToken; - String get baseURL => - _supabaseURL.isNotEmpty && - _environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - ? _supabaseURL - : _baseURL; + String get baseURL => _baseURL; SDKEnvironment get environment => _environment; String get apiKey => _apiKey; - String get supabaseKey => _supabaseKey; - - bool get isConfigured { - if (_environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { - return _supabaseURL.isNotEmpty; - } - return _baseURL.isNotEmpty; - } + bool get isConfigured => _baseURL.isNotEmpty; /// Wire in a token resolver so `requiresAuth: true` requests can /// trigger token refresh without this adapter importing the auth @@ -413,8 +378,6 @@ class HTTPClientAdapter { _environment = SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; _accessToken = null; _timeoutMs = defaultTimeoutMs; - _supabaseURL = ''; - _supabaseKey = ''; _tokenResolver = null; _refreshTokenCallback = null; } @@ -484,39 +447,31 @@ class HTTPClientAdapter { final headers = _commonsDefaultHeaders(); headers['X-Platform'] = SDKConstants.platform; - if (snapshot.environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { - if (snapshot.supabaseKey.isNotEmpty) { - headers['apikey'] = snapshot.supabaseKey; - headers['Authorization'] = 'Bearer ${snapshot.supabaseKey}'; - headers['Prefer'] = _isDeviceRegistrationPath(path) - ? 'resolution=merge-duplicates' - : 'return=representation'; - } - } else { - if (requiresAuth && snapshot.tokenResolver != null) { - try { - final token = await snapshot.tokenResolver!.call(requiresAuth: true); - if (token != null && token.isNotEmpty) { - headers['Authorization'] = 'Bearer $token'; - } else if (snapshot.apiKey.isNotEmpty) { - headers['Authorization'] = 'Bearer ${snapshot.apiKey}'; - } - } catch (_) { - _logger.debug('Token resolver failed'); - if (snapshot.apiKey.isNotEmpty) { - headers['Authorization'] = 'Bearer ${snapshot.apiKey}'; - } - } - } else { - final token = snapshot.accessToken ?? snapshot.apiKey; - if (token.isNotEmpty) { + // Every environment reaches the backend through the effective base URL and + // C++-owned auth — there is no dev-only direct-to-datastore credential path. + if (requiresAuth && snapshot.tokenResolver != null) { + try { + final token = await snapshot.tokenResolver!.call(requiresAuth: true); + if (token != null && token.isNotEmpty) { headers['Authorization'] = 'Bearer $token'; + } else if (snapshot.apiKey.isNotEmpty) { + headers['Authorization'] = 'Bearer ${snapshot.apiKey}'; + } + } catch (_) { + _logger.debug('Token resolver failed'); + if (snapshot.apiKey.isNotEmpty) { + headers['Authorization'] = 'Bearer ${snapshot.apiKey}'; } } - if (snapshot.apiKey.isNotEmpty) { - headers['apikey'] = snapshot.apiKey; + } else { + final token = snapshot.accessToken ?? snapshot.apiKey; + if (token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; } } + if (snapshot.apiKey.isNotEmpty) { + headers['apikey'] = snapshot.apiKey; + } if (extra != null) headers.addAll(extra); return headers; @@ -529,7 +484,6 @@ class HTTPClientAdapter { environment: _environment, accessToken: _accessToken, timeoutMs: _timeoutMs, - supabaseKey: _supabaseKey, tokenResolver: _tokenResolver, refreshTokenCallback: _refreshTokenCallback, ); diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart index bf0f0fbbc4..2f997444ff 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart @@ -11,7 +11,6 @@ import 'package:runanywhere/native/dart_bridge_auth.dart'; import 'package:runanywhere/native/dart_bridge_device.dart'; import 'package:runanywhere/native/dart_bridge_download.dart'; import 'package:runanywhere/native/dart_bridge_embeddings.dart'; -import 'package:runanywhere/native/dart_bridge_environment.dart'; import 'package:runanywhere/native/dart_bridge_events.dart'; import 'package:runanywhere/native/dart_bridge_file_manager.dart'; import 'package:runanywhere/native/dart_bridge_http.dart'; @@ -322,7 +321,7 @@ class DartBridge { try { final result = DartBridgeSdkInit.phase2( SdkInitPhase2Request( - buildToken: buildToken ?? DartBridgeDevConfig.buildToken ?? '', + buildToken: buildToken ?? '', forceRefreshAssignments: forceRefreshAssignments, flushTelemetry: flushTelemetry, discoverDownloadedModels: discoverDownloadedModels, diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart index 3c55e3a3b2..5d54a6ffb3 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart @@ -421,94 +421,11 @@ class ValidationResult { /// Development configuration bridge /// -/// Wraps C++ rac_dev_config.h functions for development mode with Supabase backend. -/// Credentials are stored ONLY in C++ development_config.cpp (git-ignored). +/// Wraps the canonical commons usability checks from `rac_dev_config.h` so +/// every SDK agrees on the placeholder/URL rules. The backend is reached only +/// through the effective base URL — no credentials are baked in. /// Matches Swift's `CppBridge.DevConfig` sub-namespace in `CppBridge+Environment.swift`. class DartBridgeDevConfig { - static final _logger = SDKLogger('DartBridge.DevConfig'); - - /// Check if development config is available - static bool get isAvailable { - try { - final lib = PlatformLoader.loadCommons(); - final isAvailable = lib.lookupFunction( - 'rac_dev_config_is_available', - ); - return isAvailable(); - } catch (e) { - _logger.debug('rac_dev_config_is_available not available: $e'); - return false; - } - } - - /// Get Supabase URL for development mode - /// Returns null if not configured - static String? get supabaseURL { - if (!isAvailable) return null; - - try { - final lib = PlatformLoader.loadCommons(); - final getUrl = lib - .lookupFunction Function(), Pointer Function()>( - 'rac_dev_config_get_supabase_url', - ); - - final result = getUrl(); - if (result == nullptr) return null; - return result.toDartString(); - } catch (e) { - _logger.debug('rac_dev_config_get_supabase_url not available: $e'); - return null; - } - } - - /// Get Supabase anon key for development mode - /// Returns null if not configured - static String? get supabaseKey { - if (!isAvailable) return null; - - try { - final lib = PlatformLoader.loadCommons(); - final getKey = lib - .lookupFunction Function(), Pointer Function()>( - 'rac_dev_config_get_supabase_key', - ); - - final result = getKey(); - if (result == nullptr) return null; - return result.toDartString(); - } catch (e) { - _logger.debug('rac_dev_config_get_supabase_key not available: $e'); - return null; - } - } - - /// Get build token for development mode - /// Returns null if not configured - static String? get buildToken { - try { - final lib = PlatformLoader.loadCommons(); - final hasBuildToken = lib - .lookupFunction( - 'rac_dev_config_has_build_token', - ); - - if (!hasBuildToken()) return null; - - final getToken = lib - .lookupFunction Function(), Pointer Function()>( - 'rac_dev_config_get_build_token', - ); - - final result = getToken(); - if (result == nullptr) return null; - return result.toDartString(); - } catch (e) { - _logger.debug('rac_dev_config_get_build_token not available: $e'); - return null; - } - } - /// Whether a baked-in credential is usable: non-empty and not a scaffolding /// placeholder. Delegates to the canonical commons rule /// (`rac_dev_config_is_usable_credential`) so every SDK agrees instead of diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart index 63cf83f652..070d655a5e 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart @@ -124,48 +124,11 @@ extension SDKEnvironmentExtension on SDKEnvironment { ); } -class SupabaseConfig { - final Uri projectURL; - final String anonKey; - - SupabaseConfig({required this.projectURL, required this.anonKey}); - - static SupabaseConfig? configuration(SDKEnvironment environment) { - switch (environment) { - case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: - final supabaseUrl = DartBridgeDevConfig.supabaseURL; - final supabaseKey = DartBridgeDevConfig.supabaseKey; - - // Delegate the placeholder + URL-shape checks to the canonical commons - // rule (via DartBridge) so every SDK agrees instead of each carrying its - // own regex. - if (supabaseUrl == null || - supabaseKey == null || - !DartBridgeDevConfig.isUsableHttpUrl(supabaseUrl) || - !DartBridgeDevConfig.isUsableCredential(supabaseKey)) { - return null; - } - - final uri = Uri.tryParse(supabaseUrl); - if (uri == null) { - return null; - } - - return SupabaseConfig(projectURL: uri, anonKey: supabaseKey); - default: - return null; - } - } -} - class SDKInitParams { final String apiKey; final Uri baseURL; final SDKEnvironment environment; - SupabaseConfig? get supabaseConfig => - SupabaseConfig.configuration(environment); - SDKInitParams({ required this.apiKey, required this.baseURL, @@ -210,14 +173,12 @@ class SDKInitParams { } factory SDKInitParams.forDevelopment({String apiKey = ''}) { - final supabaseConfig = SupabaseConfig.configuration( - SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, - ); + // Development mode uses local analytics; the backend is reached only + // through the effective base URL resolved by commons state, so this is a + // placeholder. Mirrors Swift's `SDKInitParams(forDevelopmentWithAPIKey:)`. return SDKInitParams( apiKey: apiKey, - baseURL: - supabaseConfig?.projectURL ?? - Uri.parse('https://dev.runanywhere.local'), + baseURL: Uri.parse('https://dev.runanywhere.local'), environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, ); } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index 8e92386a59..b6173dbe0a 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -98,7 +98,6 @@ import 'package:runanywhere/generated/voice_events.pb.dart' import 'package:runanywhere/native/dart_bridge.dart'; import 'package:runanywhere/native/dart_bridge_auth.dart'; import 'package:runanywhere/native/dart_bridge_device.dart'; -import 'package:runanywhere/native/dart_bridge_environment.dart'; import 'package:runanywhere/native/dart_bridge_events.dart'; import 'package:runanywhere/native/dart_bridge_hf_auth.dart'; import 'package:runanywhere/native/dart_bridge_model_registry.dart'; @@ -488,7 +487,8 @@ abstract final class RunAnywhere { if (environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { // Development mode ignores any caller-supplied baseURL and always uses - // the dev placeholder / Supabase-derived URL. Mirrors Swift + // the dev placeholder URL; the backend is reached only through the + // effective base URL resolved by commons state. Mirrors Swift // RunAnywhere.swift:125-127 (`SDKInitParams(forDevelopmentWithAPIKey:)`). params = SDKInitParams.forDevelopment(apiKey: apiKey ?? ''); } else { @@ -659,20 +659,16 @@ abstract final class RunAnywhere { // whatever the app passed (baked URL, keyless). final effectiveBaseURL = DartBridgeState.instance.baseURL ?? params.baseURL.toString(); + // Effective config from commons state, for every environment: staging + // resolves the baked keyless base URL, dev/prod use whatever the app + // passed. There is no direct-to-datastore path — the backend is always + // reached through this base URL. Auth stays in C++. Mirrors Swift's unified + // Phase-2 HTTP setup in RunAnywhere._performServicesInitialization(). HTTPClientAdapter.shared.configure( baseURL: effectiveBaseURL, apiKey: params.apiKey, environment: params.environment, ); - if (params.environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) { - final supabaseConfig = SupabaseConfig.configuration(params.environment); - if (supabaseConfig != null) { - HTTPClientAdapter.shared.configureDev( - supabaseURL: supabaseConfig.projectURL.toString(), - supabaseKey: supabaseConfig.anonKey, - ); - } - } // Step 2 (moved to Phase 1): the model-paths base directory is now set // inside [initializeWithParams] before it returns — see the Swift @@ -706,10 +702,6 @@ abstract final class RunAnywhere { apiKey: params.apiKey, baseURL: params.baseURL.toString(), deviceId: telemetryDeviceId, - buildToken: - params.environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - ? DartBridgeDevConfig.buildToken - : null, forceRefreshAssignments: false, flushTelemetry: true, discoverDownloadedModels: true, From 6d8e385a0e7c56304bcbe541d7071e165b176cc4 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:16:21 -0700 Subject: [PATCH 28/44] commons+ios: remove dead Supabase upsert HTTP plumbing (keep HuggingFace auth) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Supabase-style upsert path (on_conflict URL rewrite + Prefer header, armed via rac_http_request_set_upsert_mode) is unreachable now that no environment talks to Supabase — dev device registration routes through the backend like staging/prod. Removes the whole mechanism; HuggingFace bearer-token injection in the same dispatch sites is preserved verbatim. - commons: delete rac_http_upsert_mode.{h,cpp} + test_http_upsert_mode.cpp; drop rac_http_request_set_upsert_mode from rac_http_client.h + exports; strip the upsert branch from prepare_request in rac_http_client_{default,emscripten}.cpp (HF path untouched); remove RAC_ENDPOINT_DEV_DEVICE_REGISTER + collapse rac_endpoint_device_registration to the backend endpoint for all envs; drop the source + test from CMake. - iOS: remove upsertField threading + set_upsert_mode call from HTTPClientAdapter; sync the vendored CRACommons/rac_http_client.h mirror. Commons core build-verified (librac_commons.a links; strings clean). HuggingFace auth (hf_bearer_for_url / Authorization) intact. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- sdk/runanywhere-commons/CMakeLists.txt | 5 - .../exports/RACommons.exports | 1 - .../rac/infrastructure/http/rac_http_client.h | 43 -- .../infrastructure/network/rac_endpoints.h | 6 - .../http/rac_http_client_default.cpp | 39 +- .../http/rac_http_client_emscripten.cpp | 29 +- .../http/rac_http_upsert_mode.cpp | 121 ------ .../http/rac_http_upsert_mode.h | 60 --- .../src/infrastructure/network/endpoints.cpp | 12 +- sdk/runanywhere-commons/tests/CMakeLists.txt | 13 - .../tests/test_http_upsert_mode.cpp | 390 ------------------ .../CRACommons/include/rac_http_client.h | 18 - .../Foundation/Bridge/HTTPClientAdapter.swift | 18 +- 13 files changed, 21 insertions(+), 734 deletions(-) delete mode 100644 sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.cpp delete mode 100644 sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.h delete mode 100644 sdk/runanywhere-commons/tests/test_http_upsert_mode.cpp diff --git a/sdk/runanywhere-commons/CMakeLists.txt b/sdk/runanywhere-commons/CMakeLists.txt index a2de169d75..3b486f7e60 100644 --- a/sdk/runanywhere-commons/CMakeLists.txt +++ b/sdk/runanywhere-commons/CMakeLists.txt @@ -814,11 +814,6 @@ set(RAC_INFRASTRUCTURE_SOURCES # rac::http::execute / execute_stream. The facade routes through the # platform transport vtable above and has no portable HTTP fallback. src/infrastructure/http/rac_http_internal.cpp - # Supabase-style upsert request flag. Lets platform SDKs - # arm a request via `rac_http_request_set_upsert_mode`; the dispatch - # sites in rac_http_client_default.cpp / rac_http_client_emscripten.cpp - # consume it and apply the on_conflict URL rewrite + Prefer header. - src/infrastructure/http/rac_http_upsert_mode.cpp # Process-wide Hugging Face token (rac_http_hf_token_set + HF_TOKEN env # fallback). The dispatch sites attach `Authorization: Bearer` to # huggingface.co/hf.co requests so downloads, HEAD preflight, and the diff --git a/sdk/runanywhere-commons/exports/RACommons.exports b/sdk/runanywhere-commons/exports/RACommons.exports index ee88a03343..971126c498 100644 --- a/sdk/runanywhere-commons/exports/RACommons.exports +++ b/sdk/runanywhere-commons/exports/RACommons.exports @@ -433,7 +433,6 @@ _rac_http_download_execute _rac_http_hf_token_set _rac_http_hf_token_is_configured _rac_http_request_send -_rac_http_request_set_upsert_mode _rac_http_request_stream _rac_http_response_free _rac_http_transport_is_registered diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/http/rac_http_client.h b/sdk/runanywhere-commons/include/rac/infrastructure/http/rac_http_client.h index d005cfacea..5ffa7aa860 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/http/rac_http_client.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/http/rac_http_client.h @@ -216,49 +216,6 @@ RAC_API rac_result_t rac_http_request_resume(rac_http_client_t* c, const rac_htt */ RAC_API void rac_http_response_free(rac_http_response_t* resp); -// ============================================================================= -// REQUEST OPTIONS — UPSERT MODE -// ============================================================================= - -/** - * @brief Configures a request for Supabase-style upsert mode. - * - * When the request is later submitted via `rac_http_request_send`, - * `rac_http_request_stream`, or `rac_http_request_resume`, the HTTP - * client will transparently: - * - append `?on_conflict=` (or - * `&on_conflict=` if the URL already carries a - * query string) to the request URL, and - * - emit the header - * `Prefer: resolution=merge-duplicates,return=representation` - * in addition to any headers already attached to the request. - * - * This lets platform SDKs route Supabase device-registration upserts - * through the standard `rac_http_request_*` ABI without hard-coding the - * Supabase wire protocol on each platform. Pair with the SDK's - * higher-level error handling to treat HTTP 409 as a benign "already - * registered" outcome where appropriate. - * - * `on_conflict_field` is copied internally; the caller may free the - * string immediately after this call returns. Passing - * `on_conflict_field == NULL` clears any previously-set upsert mode for - * this request pointer. - * - * The flag is keyed by the `rac_http_request_t*` pointer for the - * duration of one dispatch — the next `rac_http_request_send/stream/ - * resume` consumes (and clears) it. Re-arm before each request if - * upsert behavior is required for multiple submissions of the same - * struct. - * - * @param req Non-NULL request descriptor. - * @param on_conflict_field Column name used as the conflict key - * (e.g. "device_id"); may be NULL to disable upsert mode. - * @return RAC_SUCCESS on success, RAC_ERROR_INVALID_ARGUMENT if - * `req == NULL`, RAC_ERROR_OUT_OF_MEMORY on allocation failure. - */ -RAC_API rac_result_t rac_http_request_set_upsert_mode(rac_http_request_t* req, - const char* on_conflict_field); - // ============================================================================= // CANONICAL DEFAULT HEADERS // ============================================================================= diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_endpoints.h b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_endpoints.h index 2c4352062e..09a3b9e22e 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_endpoints.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_endpoints.h @@ -34,12 +34,6 @@ extern "C" { // "model"), e.g. "/api/v2/sdk/telemetry/llm". #define RAC_ENDPOINT_TELEMETRY_V2_PREFIX "/api/v2/sdk/telemetry/" -// ============================================================================= -// Device Management - Development (Supabase REST API) -// ============================================================================= - -#define RAC_ENDPOINT_DEV_DEVICE_REGISTER "/rest/v1/sdk_devices" - // ============================================================================= // Model Management // ============================================================================= diff --git a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_default.cpp b/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_default.cpp index ed044d3e4f..0c900c17b6 100644 --- a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_default.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_default.cpp @@ -28,7 +28,6 @@ #include "rac_http_hf_auth.h" #include "rac_http_transport_ref.h" -#include "rac_http_upsert_mode.h" #include #include @@ -86,18 +85,15 @@ extern "C" void rac_http_client_destroy(rac_http_client_t* c) { namespace { -/// Result of merging an upsert transform into a request descriptor. -/// `effective_request` is the descriptor to hand to the adapter — it -/// either references `req` directly (when no transform was armed) or a -/// stack-built copy with rewritten URL + augmented headers. +/// Descriptor handed to the platform transport. `effective_request` either +/// references `req` directly (no rewrite) or a stack-built copy with the +/// Hugging Face bearer Authorization header appended. /// /// `header_storage` and the `effective_request.headers` array members /// must out-live the dispatch call; both live in this struct. struct PreparedRequest { rac_http_request_t effective_request{}; std::vector header_storage; // valid only when transformed - std::string url_storage; // backing for transformed url - std::string prefer_value_storage; // backing for "Prefer" header std::string auth_value_storage; // backing for "Authorization" header bool transformed = false; }; @@ -112,15 +108,13 @@ bool has_authorization_header(const rac_http_request_t* req) { return false; } -/// Builds the descriptor passed to the platform transport. When upsert -/// mode is engaged we rewrite the URL and append a `Prefer` header; when a -/// Hugging Face token is configured and the URL is an HF host we append the -/// bearer Authorization header; otherwise we pass `*req` through unchanged. +/// Builds the descriptor passed to the platform transport. When a Hugging +/// Face token is configured and the URL is an HF host we append the bearer +/// Authorization header; otherwise we pass `*req` through unchanged. PreparedRequest prepare_request(const rac_http_request_t* req) { PreparedRequest prepared; - auto transform = rac::http::consume_upsert_transform(req); std::string hf_bearer = rac::http::hf_bearer_for_url(req->url, has_authorization_header(req)); - if (!transform.engaged && hf_bearer.empty()) { + if (hf_bearer.empty()) { prepared.effective_request = *req; return prepared; } @@ -128,23 +122,14 @@ PreparedRequest prepare_request(const rac_http_request_t* req) { prepared.transformed = true; prepared.effective_request = *req; - // Copy existing headers, then append the armed extras. - prepared.header_storage.reserve(req->header_count + 2); + // Copy existing headers, then append the HF bearer Authorization header. + prepared.header_storage.reserve(req->header_count + 1); for (size_t i = 0; i < req->header_count; ++i) { prepared.header_storage.push_back(req->headers[i]); } - if (transform.engaged) { - prepared.url_storage = std::move(transform.transformed_url); - prepared.prefer_value_storage = std::move(transform.prefer_header_value); - prepared.header_storage.push_back( - rac_http_header_kv_t{"Prefer", prepared.prefer_value_storage.c_str()}); - prepared.effective_request.url = prepared.url_storage.c_str(); - } - if (!hf_bearer.empty()) { - prepared.auth_value_storage = std::move(hf_bearer); - prepared.header_storage.push_back( - rac_http_header_kv_t{"Authorization", prepared.auth_value_storage.c_str()}); - } + prepared.auth_value_storage = std::move(hf_bearer); + prepared.header_storage.push_back( + rac_http_header_kv_t{"Authorization", prepared.auth_value_storage.c_str()}); prepared.effective_request.headers = prepared.header_storage.data(); prepared.effective_request.header_count = prepared.header_storage.size(); diff --git a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_emscripten.cpp b/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_emscripten.cpp index b64047e2fa..fff06fc4e3 100644 --- a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_emscripten.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/http/rac_http_client_emscripten.cpp @@ -58,7 +58,6 @@ #include "rac_http_hf_auth.h" #include "rac_http_transport_ref.h" -#include "rac_http_upsert_mode.h" #include #include @@ -405,16 +404,12 @@ struct rac_http_client { namespace { -/// Mirror of `PreparedRequest` in `rac_http_client_default.cpp` — see -/// `rac_http_upsert_mode.h` for rationale. Both dispatch sites apply the -/// same Supabase-style URL/header rewrite when a request was armed via -/// `rac_http_request_set_upsert_mode` so behaviour is identical across -/// native and WASM targets. +/// Mirror of `PreparedRequest` in `rac_http_client_default.cpp`. Attaches +/// the Hugging Face bearer Authorization header when configured so behaviour +/// is identical across native and WASM targets. struct PreparedRequest { rac_http_request_t effective_request{}; std::vector header_storage; - std::string url_storage; - std::string prefer_value_storage; std::string auth_value_storage; bool transformed = false; }; @@ -423,24 +418,6 @@ PreparedRequest prepare_request(const rac_http_request_t* req) { PreparedRequest prepared; prepared.effective_request = *req; - auto transform = rac::http::consume_upsert_transform(req); - if (transform.engaged) { - prepared.transformed = true; - prepared.url_storage = std::move(transform.transformed_url); - prepared.prefer_value_storage = std::move(transform.prefer_header_value); - - prepared.header_storage.reserve(req->header_count + 2); - for (size_t i = 0; i < req->header_count; ++i) { - prepared.header_storage.push_back(req->headers[i]); - } - prepared.header_storage.push_back( - rac_http_header_kv_t{"Prefer", prepared.prefer_value_storage.c_str()}); - - prepared.effective_request.url = prepared.url_storage.c_str(); - prepared.effective_request.headers = prepared.header_storage.data(); - prepared.effective_request.header_count = prepared.header_storage.size(); - } - auto bearer = rac::http::hf_bearer_for_url( prepared.effective_request.url, has_authorization_header(&prepared.effective_request)); if (!bearer.empty()) { diff --git a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.cpp b/sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.cpp deleted file mode 100644 index a145da7134..0000000000 --- a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @file rac_http_upsert_mode.cpp - * @brief Implementation of the Supabase-style upsert request flag. - * - * Lets callers tag a `rac_http_request_t*` as an UPSERT before - * submitting it through `rac_http_request_send/stream/resume`. The - * dispatch sites consume the tag and apply the URL / `Prefer` header - * rewrite before handing off to the registered platform transport - * adapter. This pulls the Supabase wire-protocol shape into commons - * so every SDK has a single ABI to call instead of hand-rolling the - * same upsert path-rewrite logic per platform. - * - * The mapping is keyed by the `rac_http_request_t*` pointer so the - * stable public struct never has to change. State is consumed (cleared) - * on the first `consume_upsert_transform` call so a second send of the - * same struct without a fresh `set_upsert_mode` will not silently - * inherit the previous configuration. - * - * Thread-safety: backed by a Meyers singleton + `std::mutex`. Any - * thread can arm or consume; cross-thread arming is allowed. - */ - -#include "rac_http_upsert_mode.h" - -#include -#include -#include - -#include "rac/core/rac_error.h" -#include "rac/core/rac_types.h" -#include "rac/infrastructure/http/rac_http_client.h" - -namespace { - -/// Per-request upsert configuration kept while the caller arms the -/// request. The dispatch path reads (and removes) the entry. -struct UpsertEntry { - std::string on_conflict_field; -}; - -/// Global registry of armed requests. Meyers singleton ensures -/// initialization-order safety; the mutex protects all accesses. -struct Registry { - std::mutex mu; - std::unordered_map entries; -}; - -Registry& registry() { - static Registry r; - return r; -} - -/// Build the rewritten URL — appends `on_conflict=` as either -/// the first query parameter (`?...`) or an additional one (`&...`). -std::string build_upsert_url(const char* original_url, const std::string& on_conflict_field) { - std::string url = original_url ? original_url : ""; - const bool has_query = url.find('?') != std::string::npos; - url.append(has_query ? "&on_conflict=" : "?on_conflict="); - url.append(on_conflict_field); - return url; -} - -} // namespace - -// ============================================================================= -// Public C ABI — set_upsert_mode -// ============================================================================= - -extern "C" rac_result_t rac_http_request_set_upsert_mode(rac_http_request_t* req, - const char* on_conflict_field) { - if (req == nullptr) { - return RAC_ERROR_INVALID_ARGUMENT; - } - auto& reg = registry(); - std::lock_guard lock(reg.mu); - - if (on_conflict_field == nullptr) { - // Explicit "clear" — disarm any previous arming for this pointer. - reg.entries.erase(req); - return RAC_SUCCESS; - } - - try { - reg.entries[req] = UpsertEntry{std::string(on_conflict_field)}; - } catch (const std::bad_alloc&) { - return RAC_ERROR_OUT_OF_MEMORY; - } - return RAC_SUCCESS; -} - -// ============================================================================= -// Internal helpers — consume_upsert_transform -// ============================================================================= - -namespace rac::http { - -UpsertTransform consume_upsert_transform(const rac_http_request_t* req) { - UpsertTransform out; - if (req == nullptr) { - return out; - } - - auto& reg = registry(); - std::string on_conflict; - { - std::lock_guard lock(reg.mu); - auto it = reg.entries.find(req); - if (it == reg.entries.end()) { - return out; - } - on_conflict = std::move(it->second.on_conflict_field); - reg.entries.erase(it); - } - - out.engaged = true; - out.transformed_url = build_upsert_url(req->url, on_conflict); - out.prefer_header_value = "resolution=merge-duplicates,return=representation"; - return out; -} - -} // namespace rac::http diff --git a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.h b/sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.h deleted file mode 100644 index 55116e3eaa..0000000000 --- a/sdk/runanywhere-commons/src/infrastructure/http/rac_http_upsert_mode.h +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @file rac_http_upsert_mode.h - * @brief Internal helpers for the Supabase-style "upsert mode" request - * flag set via `rac_http_request_set_upsert_mode`. - * - * The public C ABI (in `rac_http_client.h`) lets callers tag a - * `rac_http_request_t*` as an UPSERT before submitting it. The actual - * URL / header transformation lives here so that every dispatch site - * (`rac_http_client_default.cpp` for native targets, - * `rac_http_client_emscripten.cpp` for WASM) can apply it uniformly. - * - * Why a side table? — `rac_http_request_t` is a flat ABI-stable C - * struct already in use by every SDK. Adding fields is breaking; - * keying upsert state by the `rac_http_request_t*` pointer keeps the - * struct untouched while still letting the dispatch layer pull the - * config back out before calling the registered platform transport. - * - * NOT part of the public C ABI. C++-only. - */ - -#pragma once - -#include - -#include "rac/core/rac_types.h" -#include "rac/infrastructure/http/rac_http_client.h" - -namespace rac::http { - -/** - * @brief Snapshot of upsert-mode state for a single dispatch. - * - * `engaged == true` means a previous call to - * `rac_http_request_set_upsert_mode` armed this request. `transformed_url` - * is the URL after the `on_conflict=` query parameter has been - * appended. `prefer_header_value` is the literal value to send under the - * `Prefer:` header. - * - * When `engaged == false` the other fields are empty and the dispatch - * site should pass the request through unchanged. - */ -struct UpsertTransform { - bool engaged = false; - std::string transformed_url; - std::string prefer_header_value; -}; - -/** - * @brief Pulls upsert state for `req` (if any) and clears the entry. - * - * Returns a `UpsertTransform` describing the rewrite to apply for this - * dispatch. Calling this consumes the entry — a second call for the - * same `req*` returns `engaged = false`. Callers that need to re-issue - * the same request must re-arm with `rac_http_request_set_upsert_mode`. - * - * Thread-safe: backed by a mutex-guarded global map. - */ -UpsertTransform consume_upsert_transform(const rac_http_request_t* req); - -} // namespace rac::http diff --git a/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp b/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp index 1d314c3a56..05154ab0b1 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/network/endpoints.cpp @@ -11,14 +11,10 @@ #include "rac/infrastructure/network/rac_endpoints.h" const char* rac_endpoint_device_registration(rac_environment_t env) { - switch (env) { - case RAC_ENV_DEVELOPMENT: - return RAC_ENDPOINT_DEV_DEVICE_REGISTER; - case RAC_ENV_STAGING: - case RAC_ENV_PRODUCTION: - default: - return RAC_ENDPOINT_DEVICE_REGISTER; - } + // Every environment registers through the backend now; there is no + // direct-to-datastore path. The parameter is retained for ABI stability. + (void)env; + return RAC_ENDPOINT_DEVICE_REGISTER; } const char* rac_endpoint_model_assignments(void) { diff --git a/sdk/runanywhere-commons/tests/CMakeLists.txt b/sdk/runanywhere-commons/tests/CMakeLists.txt index 2d50113674..9aa5526e00 100644 --- a/sdk/runanywhere-commons/tests/CMakeLists.txt +++ b/sdk/runanywhere-commons/tests/CMakeLists.txt @@ -1335,19 +1335,6 @@ if(UNIX) add_test(NAME http_download_tests COMMAND test_http_download) endif() -# --- Rac_http_request_set_upsert_mode parity test --------------- -# Pure ABI / dispatch test — registers a stub rac_http_transport_ops_t, -# arms the upsert flag, and verifies the URL/header rewrite the SDKs rely -# on. No sockets, so it builds on every platform. -add_executable(test_http_upsert_mode test_http_upsert_mode.cpp) -target_include_directories(test_http_upsert_mode PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_SOURCE_DIR}/include -) -target_link_libraries(test_http_upsert_mode PRIVATE rac_commons) -rac_link_archive_deps(test_http_upsert_mode) -target_compile_features(test_http_upsert_mode PRIVATE cxx_std_17) -add_test(NAME http_upsert_mode_tests COMMAND test_http_upsert_mode) # --- Rac_http_default_headers parity test ---------------------- # Pure ABI test — verifies the canonical default-header list matches diff --git a/sdk/runanywhere-commons/tests/test_http_upsert_mode.cpp b/sdk/runanywhere-commons/tests/test_http_upsert_mode.cpp deleted file mode 100644 index 0db26cafbc..0000000000 --- a/sdk/runanywhere-commons/tests/test_http_upsert_mode.cpp +++ /dev/null @@ -1,390 +0,0 @@ -/** - * @file test_http_upsert_mode.cpp - * @brief Parity test for `rac_http_request_set_upsert_mode`. - * - * Registers a stub `rac_http_transport_ops_t` that captures the URL + - * headers the request layer hands it, then verifies that: - * 1. Without arming, the request goes through unchanged. - * 2. After `rac_http_request_set_upsert_mode(req, "device_id")`, the - * adapter sees `?on_conflict=device_id` appended to the URL and a - * `Prefer: resolution=merge-duplicates,return=representation` - * header. - * 3. Pre-existing query strings yield `&on_conflict=...`. - * 4. Pre-existing headers are preserved when the Prefer header is - * appended. - * 5. Calling `set_upsert_mode(req, NULL)` clears prior arming. - * 6. Arming is single-shot: a second send without re-arming reverts - * to the un-rewritten URL/headers. - * 7. Stream + resume dispatch sites apply the same transform as send. - */ - -#include -#include -#include -#include -#include -#include - -#include "rac/core/rac_error.h" -#include "rac/core/rac_types.h" -#include "rac/infrastructure/http/rac_http_client.h" -#include "rac/infrastructure/http/rac_http_transport.h" - -namespace { - -// ============================================================================= -// Capture struct — every adapter call writes the inputs it observed here. -// ============================================================================= - -struct CaptureState { - std::string url; - std::vector> headers; - std::string method; - std::string body; - int call_count = 0; -}; - -CaptureState g_capture; - -void reset_capture() { - g_capture = CaptureState{}; -} - -// ============================================================================= -// Stub adapter — records inputs, returns a synthetic 200 response. -// ============================================================================= - -void copy_request(const rac_http_request_t* req) { - g_capture.method = req->method ? req->method : ""; - g_capture.url = req->url ? req->url : ""; - g_capture.headers.clear(); - for (size_t i = 0; i < req->header_count; ++i) { - g_capture.headers.emplace_back(req->headers[i].name ? req->headers[i].name : "", - req->headers[i].value ? req->headers[i].value : ""); - } - g_capture.body.assign(reinterpret_cast(req->body_bytes), - req->body_bytes ? req->body_len : 0); - ++g_capture.call_count; -} - -rac_result_t stub_request_send(void* /*user_data*/, const rac_http_request_t* req, - rac_http_response_t* out_resp) { - copy_request(req); - out_resp->status = 200; - return RAC_SUCCESS; -} - -rac_result_t stub_request_stream(void* /*user_data*/, const rac_http_request_t* req, - rac_http_body_chunk_fn /*cb*/, void* /*cb_user_data*/, - rac_http_response_t* out_resp_meta) { - copy_request(req); - out_resp_meta->status = 200; - return RAC_SUCCESS; -} - -rac_result_t stub_request_resume(void* /*user_data*/, const rac_http_request_t* req, - uint64_t /*resume_from_byte*/, rac_http_body_chunk_fn /*cb*/, - void* /*cb_user_data*/, rac_http_response_t* out_resp_meta) { - copy_request(req); - out_resp_meta->status = 206; - return RAC_SUCCESS; -} - -const rac_http_transport_ops_t kStubOps = { - /*request_send=*/stub_request_send, - /*request_stream=*/stub_request_stream, - /*request_resume=*/stub_request_resume, - /*init=*/nullptr, - /*destroy=*/nullptr, -}; - -// ============================================================================= -// Tiny assert machinery (mirrors test_http_client.cpp). -// ============================================================================= - -int g_failures = 0; -int g_passes = 0; - -#define CHECK(cond) \ - do { \ - if (cond) { \ - ++g_passes; \ - } else { \ - ++g_failures; \ - std::cerr << "[FAIL] " << __FILE__ << ":" << __LINE__ << " - " #cond << "\n"; \ - } \ - } while (0) - -#define CHECK_EQ_I(a, b) \ - do { \ - auto _a = (a); \ - auto _b = (b); \ - if (_a == _b) { \ - ++g_passes; \ - } else { \ - ++g_failures; \ - std::cerr << "[FAIL] " << __FILE__ << ":" << __LINE__ << " - " #a " == " #b " (got " \ - << _a << " vs " << _b << ")\n"; \ - } \ - } while (0) - -#define CHECK_EQ_S(a, b) \ - do { \ - std::string _a = (a); \ - std::string _b = (b); \ - if (_a == _b) { \ - ++g_passes; \ - } else { \ - ++g_failures; \ - std::cerr << "[FAIL] " << __FILE__ << ":" << __LINE__ << " - " #a " == " #b " (got '" \ - << _a << "' vs '" << _b << "')\n"; \ - } \ - } while (0) - -bool find_header(const std::string& name, std::string* value_out) { - for (auto& kv : g_capture.headers) { - if (kv.first == name) { - if (value_out) - *value_out = kv.second; - return true; - } - } - return false; -} - -// ============================================================================= -// Helpers: build a stock POST request descriptor. -// ============================================================================= - -rac_http_request_t make_request(const char* url, const rac_http_header_kv_t* headers, - size_t header_count, const char* body) { - rac_http_request_t req{}; - req.method = "POST"; - req.url = url; - req.headers = headers; - req.header_count = header_count; - req.body_bytes = body ? reinterpret_cast(body) : nullptr; - req.body_len = body ? std::strlen(body) : 0; - req.timeout_ms = 5000; - req.follow_redirects = RAC_FALSE; - req.expected_checksum_hex = nullptr; - return req; -} - -// ============================================================================= -// Tests -// ============================================================================= - -void test_passthrough_when_not_armed() { - reset_capture(); - rac_http_client_t* client = nullptr; - CHECK_EQ_I(rac_http_client_create(&client), RAC_SUCCESS); - - rac_http_request_t req = make_request("https://example.test/devices", nullptr, 0, "{}"); - rac_http_response_t resp{}; - CHECK_EQ_I(rac_http_request_send(client, &req, &resp), RAC_SUCCESS); - - // URL untouched, no Prefer header. - CHECK_EQ_S(g_capture.url, "https://example.test/devices"); - CHECK(!find_header("Prefer", nullptr)); - CHECK_EQ_I(g_capture.headers.size(), size_t{0}); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -void test_upsert_simple_url_no_query() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - rac_http_request_t req = make_request("https://example.test/devices", nullptr, 0, "{}"); - CHECK_EQ_I(rac_http_request_set_upsert_mode(&req, "device_id"), RAC_SUCCESS); - - rac_http_response_t resp{}; - CHECK_EQ_I(rac_http_request_send(client, &req, &resp), RAC_SUCCESS); - - // URL gets `?on_conflict=device_id`. - CHECK_EQ_S(g_capture.url, "https://example.test/devices?on_conflict=device_id"); - - // Prefer header set with the canonical Supabase value. - std::string prefer_value; - CHECK(find_header("Prefer", &prefer_value)); - CHECK_EQ_S(prefer_value, "resolution=merge-duplicates,return=representation"); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -void test_upsert_url_with_existing_query() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - // Pre-existing `?select=*` — upsert must use `&` not `?`. - rac_http_request_t req = - make_request("https://example.test/devices?select=*", nullptr, 0, "{}"); - rac_http_request_set_upsert_mode(&req, "device_id"); - - rac_http_response_t resp{}; - rac_http_request_send(client, &req, &resp); - - CHECK_EQ_S(g_capture.url, "https://example.test/devices?select=*&on_conflict=device_id"); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -void test_upsert_preserves_existing_headers() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - rac_http_header_kv_t hs[2] = { - {"Content-Type", "application/json"}, - {"X-API-Key", "secret-token"}, - }; - rac_http_request_t req = make_request("https://example.test/devices", hs, 2, "{}"); - rac_http_request_set_upsert_mode(&req, "device_id"); - - rac_http_response_t resp{}; - rac_http_request_send(client, &req, &resp); - - // Original headers must survive... - std::string ct, key; - CHECK(find_header("Content-Type", &ct)); - CHECK_EQ_S(ct, "application/json"); - CHECK(find_header("X-API-Key", &key)); - CHECK_EQ_S(key, "secret-token"); - - // ...and Prefer is appended. - std::string prefer_value; - CHECK(find_header("Prefer", &prefer_value)); - CHECK_EQ_S(prefer_value, "resolution=merge-duplicates,return=representation"); - - // 2 original + 1 Prefer = 3 headers visible to the adapter. - CHECK_EQ_I(g_capture.headers.size(), size_t{3}); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -void test_upsert_clear_with_null() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - rac_http_request_t req = make_request("https://example.test/devices", nullptr, 0, "{}"); - // Arm then clear. - CHECK_EQ_I(rac_http_request_set_upsert_mode(&req, "device_id"), RAC_SUCCESS); - CHECK_EQ_I(rac_http_request_set_upsert_mode(&req, nullptr), RAC_SUCCESS); - - rac_http_response_t resp{}; - rac_http_request_send(client, &req, &resp); - - // No transform should have been applied. - CHECK_EQ_S(g_capture.url, "https://example.test/devices"); - CHECK(!find_header("Prefer", nullptr)); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -void test_upsert_is_single_shot() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - rac_http_request_t req = make_request("https://example.test/devices", nullptr, 0, "{}"); - rac_http_request_set_upsert_mode(&req, "device_id"); - - // First send — transform applies. - rac_http_response_t r1{}; - rac_http_request_send(client, &req, &r1); - CHECK_EQ_S(g_capture.url, "https://example.test/devices?on_conflict=device_id"); - CHECK(find_header("Prefer", nullptr)); - rac_http_response_free(&r1); - - // Second send without re-arming — the per-request flag has been consumed. - reset_capture(); - rac_http_response_t r2{}; - rac_http_request_send(client, &req, &r2); - CHECK_EQ_S(g_capture.url, "https://example.test/devices"); - CHECK(!find_header("Prefer", nullptr)); - rac_http_response_free(&r2); - - rac_http_client_destroy(client); -} - -void test_upsert_invalid_args() { - CHECK_EQ_I(rac_http_request_set_upsert_mode(nullptr, "device_id"), RAC_ERROR_INVALID_ARGUMENT); -} - -void test_upsert_applied_in_stream() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - rac_http_request_t req = make_request("https://example.test/devices", nullptr, 0, "{}"); - rac_http_request_set_upsert_mode(&req, "device_id"); - - auto cb = [](const uint8_t*, size_t, uint64_t, uint64_t, void*) -> rac_bool_t { - return RAC_TRUE; - }; - - rac_http_response_t resp{}; - rac_http_request_stream(client, &req, cb, nullptr, &resp); - CHECK_EQ_S(g_capture.url, "https://example.test/devices?on_conflict=device_id"); - CHECK(find_header("Prefer", nullptr)); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -void test_upsert_applied_in_resume() { - reset_capture(); - rac_http_client_t* client = nullptr; - rac_http_client_create(&client); - - rac_http_request_t req = make_request("https://example.test/devices", nullptr, 0, "{}"); - rac_http_request_set_upsert_mode(&req, "device_id"); - - auto cb = [](const uint8_t*, size_t, uint64_t, uint64_t, void*) -> rac_bool_t { - return RAC_TRUE; - }; - - rac_http_response_t resp{}; - rac_http_request_resume(client, &req, /*resume_from_byte=*/0, cb, nullptr, &resp); - CHECK_EQ_S(g_capture.url, "https://example.test/devices?on_conflict=device_id"); - CHECK(find_header("Prefer", nullptr)); - - rac_http_response_free(&resp); - rac_http_client_destroy(client); -} - -} // namespace - -int main() { - std::cout << "=== rac_http_request_set_upsert_mode tests ===\n"; - - if (rac_http_transport_register(&kStubOps, /*user_data=*/nullptr) != RAC_SUCCESS) { - std::cerr << "failed to register stub HTTP transport\n"; - return 1; - } - - test_passthrough_when_not_armed(); - test_upsert_simple_url_no_query(); - test_upsert_url_with_existing_query(); - test_upsert_preserves_existing_headers(); - test_upsert_clear_with_null(); - test_upsert_is_single_shot(); - test_upsert_invalid_args(); - test_upsert_applied_in_stream(); - test_upsert_applied_in_resume(); - - // Unregister so we don't leak the stub vtable into any later tests. - rac_http_transport_register(nullptr, nullptr); - - std::cout << "passes=" << g_passes << " failures=" << g_failures << "\n"; - return g_failures == 0 ? 0 : 1; -} diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_http_client.h b/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_http_client.h index 6fa46785de..8fc3b5f2a2 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_http_client.h +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_http_client.h @@ -211,24 +211,6 @@ RAC_API rac_result_t rac_http_request_resume(rac_http_client_t* c, const rac_htt */ RAC_API void rac_http_response_free(rac_http_response_t* resp); -// ============================================================================= -// REQUEST OPTIONS — UPSERT MODE -// ============================================================================= - -/** - * @brief Configures a request for Supabase-style upsert mode. - * - * When the request is later submitted via `rac_http_request_send`, - * `rac_http_request_stream`, or `rac_http_request_resume`, the HTTP - * client will transparently append `?on_conflict=` to - * the URL and emit - * `Prefer: resolution=merge-duplicates,return=representation`. - * - * `on_conflict_field == NULL` clears any previously-set upsert mode. - */ -RAC_API rac_result_t rac_http_request_set_upsert_mode(rac_http_request_t* req, - const char* on_conflict_field); - // ============================================================================= // CANONICAL DEFAULT HEADERS // ============================================================================= diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift index 4718165242..3ca252054d 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift @@ -8,7 +8,6 @@ // Thin Swift bridge over the canonical `rac_http_client_*` C ABI. // All cross-platform HTTP policy lives in commons: // - `rac_http_default_headers` → canonical SDK header list -// - `rac_http_request_set_upsert_mode` → Supabase upsert semantics // - `rac_api_error_from_response` → HTTP-status → SDKException // @@ -127,7 +126,6 @@ public actor HTTPClientAdapter { urlString: url.absoluteString, apiKey: nil, authToken: nil, - upsertField: nil, body: nil, trustBoundary: .externalAsset, logger: SDKLogger(category: "HTTPClientAdapter.fetchURL") @@ -157,15 +155,11 @@ public actor HTTPClientAdapter { category: .internal ) } - // Supabase device registration uses UPSERT semantics — defer the URL - // / header rewrite to commons via `rac_http_request_set_upsert_mode`. - let upsertField: String? = path.contains(RAC_ENDPOINT_DEV_DEVICE_REGISTER) ? "device_id" : nil return try await Self.dispatch( method: method, urlString: urlString, apiKey: configuration.apiKey, authToken: token.isEmpty ? nil : token, - upsertField: upsertField, body: body, trustBoundary: .controlPlane, logger: logger @@ -221,7 +215,6 @@ public actor HTTPClientAdapter { urlString: String, apiKey: String?, authToken: String?, - upsertField: String?, body: Data?, trustBoundary: RequestTrustBoundary, logger: SDKLogger @@ -234,7 +227,6 @@ public actor HTTPClientAdapter { urlString: urlString, apiKey: apiKey, authToken: authToken, - upsertField: upsertField, body: body, trustBoundary: trustBoundary, logger: logger @@ -249,7 +241,6 @@ public actor HTTPClientAdapter { urlString: String, apiKey: String?, authToken: String?, - upsertField: String?, body: Data?, trustBoundary: RequestTrustBoundary, logger: SDKLogger @@ -301,7 +292,6 @@ public actor HTTPClientAdapter { urlC: urlC, headerKVs: kvBuf, body: body, - upsertField: upsertField, trustBoundary: trustBoundary ) } @@ -316,15 +306,14 @@ public actor HTTPClientAdapter { throw mapAPIError(statusCode: status, body: data, url: urlString) } - /// Builds the request struct, optionally arms upsert mode, dispatches, - /// and copies the response body into Swift-owned `Data`. + /// Builds the request struct, dispatches, and copies the response body + /// into Swift-owned `Data`. private static func send( client: OpaquePointer, methodC: UnsafeMutablePointer, urlC: UnsafeMutablePointer, headerKVs: UnsafeBufferPointer, body: Data?, - upsertField: String?, trustBoundary: RequestTrustBoundary ) -> (rac_result_t, Int32, Data) { func dispatchWith(bodyBase: UnsafePointer?, bodyLen: Int) -> (rac_result_t, Int32, Data) { @@ -339,9 +328,6 @@ public actor HTTPClientAdapter { follow_redirects: trustBoundary.followsRedirects ? RAC_TRUE : RAC_FALSE, expected_checksum_hex: nil ) - if let upsertField { - upsertField.withCString { _ = rac_http_request_set_upsert_mode(&request, $0) } - } var response = rac_http_response_t() let result = rac_http_request_send(client, &request, &response) defer { rac_http_response_free(&response) } From c80671957542ebb4d7b19aafd65c6840dda83ede Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:18:57 -0700 Subject: [PATCH 29/44] react-native: remove dead Supabase upsert plumbing (mirror commons/iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors commit 6d8e385a0. RN's InitBridge.postJsonViaRacHttpClient detected `/rest/v1/sdk_devices` and armed the removed commons upsert mode (rac_http_request_set_upsert_mode + on_conflict/Prefer). Since dev registration now routes through the backend endpoint, that path is dead — removed the detection + set_upsert_mode call. Normal POST path, auth headers, and the generic 409 tolerance are untouched. grep clean: no on_conflict/sdk_devices/set_upsert_mode refs remain in the package. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../packages/core/cpp/bridges/InitBridge.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp index 19918c87f5..59473935af 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp @@ -734,13 +734,6 @@ static std::tuple postJsonViaRacHttpClient( const std::string& jsonBody, const std::string& apiKey ) { - // Supabase device-registration upserts route through - // rac_http_request_set_upsert_mode below (commons appends - // ?on_conflict= and the merge-duplicates Prefer header) instead of - // duplicating the Supabase wire protocol at this layer. - const bool isDeviceUpsert = - url.find("/rest/v1/sdk_devices") != std::string::npos; - std::vector headers = { {"Content-Type", "application/json"}, {"Accept", "application/json"}, @@ -770,9 +763,6 @@ static std::tuple postJsonViaRacHttpClient( // target. Callers may retry against an explicitly validated endpoint. req.follow_redirects = RAC_FALSE; req.expected_checksum_hex = nullptr; - if (isDeviceUpsert) { - rac_http_request_set_upsert_mode(&req, "device_id"); - } rac_http_response_t resp{}; rac_result_t sendResult = rac_http_request_send(client, &req, &resp); From 83c802c11e6c7e971f44ad03c53326c18d49f37a Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:19:55 -0700 Subject: [PATCH 30/44] flutter: remove dead Supabase upsert plumbing (mirror commons/iOS) Mirrors commit 6d8e385a0. Removes the Dart-side Supabase upsert append: _maybeAppendSupabaseUpsert / _isDeviceRegistrationPath and the dev-mode on_conflict=device_id + merge-duplicates Prefer rewrite in dart_bridge_device. Device registration uses the backend endpoint (from the commons callback) for all environments; the now write-only _environment field is dropped. Normal send path, auth headers, and telemetry untouched. grep clean: no on_conflict/sdk_devices/upsert refs remain in lib. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../lib/adapters/http_client_adapter.dart | 25 +++---------------- .../lib/native/dart_bridge_device.dart | 22 +++------------- 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart index f058b1ba92..217c8c9f6f 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/adapters/http_client_adapter.dart @@ -12,7 +12,7 @@ // so the blocking libcurl call never stalls the UI thread. // * The adapter carries the SDK-level request config (baseURL, // apiKey, env, access token, default headers) so call sites don't -// have to reconstruct `Authorization` / `apikey` / `Prefer` headers +// have to reconstruct `Authorization` / `apikey` headers // themselves. import 'dart:async'; @@ -269,7 +269,7 @@ class HTTPClientAdapter { var response = await rawRequest( method: method, - url: _maybeAppendSupabaseUpsert(url, path, snapshot.environment), + url: url, headers: resolvedHeaders, body: encodedBody, timeoutMs: timeoutMs ?? snapshot.timeoutMs, @@ -294,7 +294,7 @@ class HTTPClientAdapter { retryHeaders['Authorization'] = 'Bearer $newToken'; response = await rawRequest( method: method, - url: _maybeAppendSupabaseUpsert(url, path, snapshot.environment), + url: url, headers: retryHeaders, body: encodedBody, timeoutMs: timeoutMs ?? snapshot.timeoutMs, @@ -397,25 +397,6 @@ class HTTPClientAdapter { return '$base$endpoint'; } - /// Supabase device-registration endpoints need `on_conflict=device_id` - /// to do an UPSERT instead of rejecting duplicates. - String _maybeAppendSupabaseUpsert( - String url, - String path, - SDKEnvironment environment, - ) { - if (environment != SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) return url; - if (!_isDeviceRegistrationPath(path)) return url; - final separator = url.contains('?') ? '&' : '?'; - return '$url${separator}on_conflict=device_id'; - } - - bool _isDeviceRegistrationPath(String path) { - return path.contains('sdk_devices') || - path.contains('devices/register') || - path.contains('rest/v1/sdk_devices'); - } - bool _redirectsAllowedForHeaders( bool requested, Map headers, diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart index d93251719f..a2b8d47638 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart @@ -55,10 +55,6 @@ class DartBridgeDevice { /// SharedPreferences instance (lazily initialized) static SharedPreferences? _prefs; - /// SDK environment for HTTP calls - static SDKEnvironment _environment = - SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - /// Base URL for HTTP calls static String? _baseURL; @@ -165,7 +161,6 @@ class DartBridgeDevice { String? baseURL, String? accessToken, }) async { - _environment = environment; _baseURL = baseURL; _accessToken = accessToken; @@ -271,7 +266,6 @@ class DartBridgeDevice { _cachedDeviceId = null; _cachedRegistrationInfo = _DeviceRegistrationInfoSnapshot.defaults(); _prefs = null; - _environment = SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; _baseURL = null; _accessToken = null; _pendingEndpoint = null; @@ -306,15 +300,9 @@ class DartBridgeDevice { ? baseURL.substring(0, baseURL.length - 1) : baseURL; - // For dev mode (Supabase), add ?on_conflict=device_id for UPSERT - // Matches Swift/Kotlin behavior - final isDev = _environment == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - final finalEndpoint = isDev - ? (endpoint.contains('?') - ? '$endpoint&on_conflict=device_id' - : '$endpoint?on_conflict=device_id') - : endpoint; - final fullUrl = '$trimmedBase$finalEndpoint'; + // Every environment registers through the backend endpoint (the C++ + // callback supplies the resolved path). No datastore-specific rewrite. + final fullUrl = '$trimmedBase$endpoint'; // Build headers matching Kotlin/Swift final headers = { @@ -322,10 +310,6 @@ class DartBridgeDevice { 'Accept': 'application/json', }; - if (isDev) { - headers['Prefer'] = 'resolution=merge-duplicates'; - } - final accessToken = _accessToken ?? DartBridgeAuth.instance.getAccessToken(); if (requiresAuth) { From 37e09cb5a334b03114629a1a3d7b687b0fb7c65b Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:24:39 -0700 Subject: [PATCH 31/44] web: remove dead Supabase upsert plumbing (mirror commons/iOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors commit 6d8e385a0. DeviceRegistrationAdapter no longer detects `/rest/v1/sdk_devices` to append `?on_conflict=device_id` or emit the merge-duplicates Prefer header — device registration uses the backend endpoint URL as-is for every environment. Tests updated to assert the plain `/api/v1/devices/register` POST (no on_conflict, no Prefer). Normal auth headers + two-phase retry untouched. grep clean (src+tests); tsc --noEmit: PASS. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../src/Adapters/DeviceRegistrationAdapter.ts | 19 +++----------- .../DeviceRegistrationAdapter.test.ts | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts b/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts index 841d252f91..e9b3dc5aae 100644 --- a/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts +++ b/sdk/runanywhere-web/packages/core/src/Adapters/DeviceRegistrationAdapter.ts @@ -542,7 +542,6 @@ export class DeviceRegistrationAdapter { try { const endpoint = this.module.UTF8ToString(endpointPtr); const jsonBody = this.module.UTF8ToString(jsonBodyPtr); - const isDevelopmentRegistration = endpoint.startsWith('/rest/v1/'); const controlPlane = this.currentControlPlaneConfiguration(); if (!controlPlane) { return this.writeHTTPFailure( @@ -553,8 +552,8 @@ export class DeviceRegistrationAdapter { ); } const { baseURL, apiKey } = controlPlane; - const resolvedURL = resolveControlPlaneURL(baseURL, endpoint); - if (!resolvedURL) { + const url = resolveControlPlaneURL(baseURL, endpoint); + if (!url) { return this.writeHTTPFailure( outResponsePtr, RAC_ERROR_INVALID_CONFIGURATION, @@ -563,11 +562,7 @@ export class DeviceRegistrationAdapter { ); } - const accessToken = requiresAuth !== 0 - ? this.currentAccessToken() - : isDevelopmentRegistration - ? apiKey - : ''; + const accessToken = requiresAuth !== 0 ? this.currentAccessToken() : ''; if (requiresAuth !== 0 && (!apiKey || !accessToken)) { return this.writeHTTPFailure( outResponsePtr, @@ -577,9 +572,6 @@ export class DeviceRegistrationAdapter { ); } - const url = isDevelopmentRegistration - ? `${resolvedURL}?on_conflict=device_id` - : resolvedURL; // Native reconstructs the registration payload for the retry and refreshes // volatile fields such as last_seen_at_ms. Correlate the prepared response // with the only operation this callback serves (route + auth mode), rather @@ -614,7 +606,6 @@ export class DeviceRegistrationAdapter { jsonBody, apiKey, accessToken, - isDevelopmentRegistration, }); this.pendingRequest = pending; void pending.finally(() => { @@ -676,7 +667,6 @@ export class DeviceRegistrationAdapter { jsonBody: string; apiKey: string; accessToken: string; - isDevelopmentRegistration: boolean; }): Promise { const controller = new AbortController(); this.activeRequestController = controller; @@ -691,9 +681,6 @@ export class DeviceRegistrationAdapter { }); if (options.apiKey) headers.set('apikey', options.apiKey); if (options.accessToken) headers.set('Authorization', `Bearer ${options.accessToken}`); - if (options.isDevelopmentRegistration) { - headers.set('Prefer', 'resolution=merge-duplicates,return=representation'); - } const response = await fetch(options.url, { method: 'POST', headers, diff --git a/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts b/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts index dc7a28f287..f100589bb9 100644 --- a/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts +++ b/sdk/runanywhere-web/packages/core/tests/unit/Adapters/DeviceRegistrationAdapter.test.ts @@ -353,7 +353,7 @@ describe('DeviceRegistrationAdapter', () => { adapter.cleanup(); }); - it('distinguishes HTTP rejection, development upsert, and missing configuration', async () => { + it('distinguishes HTTP rejection, development registration, and missing configuration', async () => { const prod = createFakeModule(); const prodAdapter = DeviceRegistrationAdapter.install(prod.module, { baseURL: 'https://relay.test', @@ -374,8 +374,10 @@ describe('DeviceRegistrationAdapter', () => { fetchStub.mockClear(); fetchStub.mockResolvedValueOnce({ ok: true, status: 204 } as Response); - // Dev/keyless upsert now flows through the effective base URL + API key - // supplied by commons state (no baked Supabase dev-config credentials). + // Every environment (including development/keyless) registers through the + // backend endpoint using the effective base URL + API key from commons + // state. The device-registration URL is used as-is, with no query rewrite + // and no extra request-preference header. const dev = createFakeModule(); const devAdapter = DeviceRegistrationAdapter.install(dev.module, { baseURL: 'https://development.invalid', @@ -383,16 +385,17 @@ describe('DeviceRegistrationAdapter', () => { environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, sdkVersion: '0.19.13', }); - const devEndpoint = dev.allocateString('/rest/v1/sdk_devices'); + const devEndpoint = dev.allocateString('/api/v1/devices/register'); const devBody = dev.allocateString('{}'); const devResponse = dev.allocateString(' '.repeat(RESPONSE.size)); - expect(callback(dev, CALLBACK.httpPost)(devEndpoint, devBody, 0, devResponse, 0)).toBe(-100); + expect(callback(dev, CALLBACK.httpPost)(devEndpoint, devBody, 1, devResponse, 0)).toBe(-100); await DeviceRegistrationAdapter.waitForPendingRegistration(dev.module); - expect(callback(dev, CALLBACK.httpPost)(devEndpoint, devBody, 0, devResponse, 0)).toBe(0); - expect(fetchStub.mock.calls[0][0]).toContain('?on_conflict=device_id'); + expect(callback(dev, CALLBACK.httpPost)(devEndpoint, devBody, 1, devResponse, 0)).toBe(0); + expect(fetchStub.mock.calls[0][0]).toBe('https://development.invalid/api/v1/devices/register'); const devHeaders = new Headers((fetchStub.mock.calls[0][1] as RequestInit).headers); - expect(devHeaders.get('authorization')).toBe('Bearer test-development-key'); - expect(devHeaders.get('prefer')).toContain('merge-duplicates'); + expect(devHeaders.get('apikey')).toBe('test-development-key'); + expect(devHeaders.get('authorization')).toBe('Bearer test-access-token'); + expect(devHeaders.get('prefer')).toBeNull(); devAdapter.cleanup(); fetchStub.mockClear(); @@ -401,7 +404,7 @@ describe('DeviceRegistrationAdapter', () => { environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, sdkVersion: '0.19.13', }); - const noConfigEndpoint = noConfig.allocateString('/rest/v1/sdk_devices'); + const noConfigEndpoint = noConfig.allocateString('/api/v1/devices/register'); const noConfigBody = noConfig.allocateString('{}'); const noConfigResponse = noConfig.allocateString(' '.repeat(RESPONSE.size)); expect(callback(noConfig, CALLBACK.httpPost)( @@ -428,7 +431,7 @@ describe('DeviceRegistrationAdapter', () => { environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, sdkVersion: '0.19.13', }); - const endpoint = handle.allocateString('/rest/v1/sdk_devices'); + const endpoint = handle.allocateString('/api/v1/devices/register'); const body = handle.allocateString('{}'); const response = handle.allocateString(' '.repeat(RESPONSE.size)); From 0593475aa9d179351e9a3342ea3bc81066d9716f Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:25:07 -0700 Subject: [PATCH 32/44] kotlin: remove dead Supabase upsert plumbing (mirror commons/iOS) Mirrors commit 6d8e385a0. Removes the Kotlin-side Supabase upsert path: DEV_DEVICE_REGISTER_MARKER / on_conflict detection, platformExecuteHttpUpsert + rewriteForUpsertFallback, and the merge-duplicates/return=representation Prefer header. Device registration endpoint resolution collapses to the backend endpoint for all environments (FALLBACK_DEVICE_REGISTRATION), matching commons rac_endpoint_device_registration; the dev special-case in isDeviceRegisteredCallback is dropped. Normal request path + apikey/Authorization headers untouched. grep clean across the SDK; Gradle compile pending (deps/NDK not bootstrapped here). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../foundation/bridge/HTTPClientAdapter.kt | 135 ++---------------- .../bridge/extensions/CppBridgeDevice.kt | 26 ++-- .../bridge/extensions/CppBridgeEnvironment.kt | 18 +-- .../sdk/native/bridge/RunAnywhereBridge.kt | 4 - 4 files changed, 28 insertions(+), 155 deletions(-) diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt index ad65f5685c..90510d7b46 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt @@ -9,7 +9,6 @@ * * All cross-platform HTTP policy lives in commons: * - `rac_http_default_headers` → canonical SDK header list - * - `rac_http_request_set_upsert_mode` → Supabase upsert semantics * - `rac_api_error_from_response` → HTTP-status → SDKException * * SDK-level HTTP requests (auth, device registration, telemetry) route @@ -75,21 +74,6 @@ internal data class ApiErrorInfo( public object HTTPClientAdapter { private const val DEFAULT_TIMEOUT_MS: Int = 30_000 - /** - * Supabase device-registration endpoint marker (mirrors Swift's - * `RAC_ENDPOINT_DEV_DEVICE_REGISTER` = - * `"/rest/v1/sdk_devices"`). Path-substring match triggers the - * upsert-mode rewrite on the C side. - */ - private const val DEV_DEVICE_REGISTER_MARKER: String = "/rest/v1/sdk_devices" - - /** - * Conflict-key column for the device-registration upsert (mirrors - * Swift's `"device_id"` literal passed to - * `rac_http_request_set_upsert_mode`). - */ - private const val DEV_DEVICE_REGISTER_UPSERT_FIELD: String = "device_id" - private val logger = SDKLogger("HTTPClientAdapter") private val stateLock = Any() @@ -186,7 +170,7 @@ public object HTTPClientAdapter { url: String, timeoutMs: Int = DEFAULT_TIMEOUT_MS, ): ByteArray { - val headers = buildHeaders(apiKey = null, authToken = null, upsert = false) + val headers = buildHeaders(apiKey = null, authToken = null) val result = platformExecuteHttp( method = "GET", @@ -212,47 +196,27 @@ public object HTTPClientAdapter { val url = buildURL(base = snapshot.baseURL, path = path) val token = resolveToken(requiresAuth = requiresAuth, apiKey = snapshot.apiKey) requireCurrentConfiguration(snapshot) - val isUpsert = path.contains(DEV_DEVICE_REGISTER_MARKER) val headers = buildHeaders( apiKey = snapshot.apiKey, authToken = token.ifEmpty { null }, - upsert = isUpsert, ) - val headerKeys = headers.keys.toTypedArray() - val headerValues = headers.values.toTypedArray() - val result = - if (isUpsert) { - // Supabase upsert path — defer URL + Prefer header rewrite - // to commons via `rac_http_request_set_upsert_mode`. - platformExecuteHttpUpsert( - method = method, - url = url, - headerKeys = headerKeys, - headerValues = headerValues, - body = body, - timeoutMs = DEFAULT_TIMEOUT_MS, - // Control-plane requests carry the API key and may also - // contain device-registration metadata/build tokens. - // Fail on redirects so no custom credential or payload is - // replayed to a different origin. - followRedirects = false, - onConflictField = DEV_DEVICE_REGISTER_UPSERT_FIELD, - ) - } else { - platformExecuteHttp( - method = method, - url = url, - headerKeys = headerKeys, - headerValues = headerValues, - body = body, - timeoutMs = DEFAULT_TIMEOUT_MS, - followRedirects = false, - ) - } + platformExecuteHttp( + method = method, + url = url, + headerKeys = headers.keys.toTypedArray(), + headerValues = headers.values.toTypedArray(), + body = body, + timeoutMs = DEFAULT_TIMEOUT_MS, + // Control-plane requests carry the API key and may also + // contain device-registration metadata/build tokens. Fail on + // redirects so no custom credential or payload is replayed to + // a different origin. + followRedirects = false, + ) requireCurrentConfiguration(snapshot) return interpretResult(result, method = method, url = url) } @@ -362,7 +326,6 @@ public object HTTPClientAdapter { private fun buildHeaders( apiKey: String?, authToken: String?, - upsert: Boolean, ): LinkedHashMap { val headers = LinkedHashMap(8) val canonical = platformDefaultHeaders() @@ -385,13 +348,6 @@ public object HTTPClientAdapter { headers["X-Platform"] = SDK_PLATFORM if (apiKey != null) { headers["apikey"] = apiKey - // Supabase PostgREST: include the inserted/updated row in - // the response body. Mirrors Swift's identical line. - // For upserts, the commons-side `rac_http_request_set_upsert_mode` - // rewrites this header to add `resolution=merge-duplicates`. - if (!upsert) { - headers["Prefer"] = "return=representation" - } } if (authToken != null) { headers["Authorization"] = "Bearer $authToken" @@ -483,39 +439,6 @@ internal suspend fun platformExecuteHttp( nativeHttpResponseToResult(resp) } -@Suppress("UnusedParameter") -internal suspend fun platformExecuteHttpUpsert( - method: String, - url: String, - headerKeys: Array, - headerValues: Array, - body: ByteArray?, - timeoutMs: Int, - followRedirects: Boolean, - onConflictField: String, -): HttpExecutionResult = - withContext(Dispatchers.IO) { - // The commons C API does not expose an upsert-mode HTTP variant, so - // the upsert request is emitted via the standard execute path with - // a Kotlin-side Prefer-header rewrite to advertise the Supabase - // `resolution=merge-duplicates` policy expected by PostgREST. The - // `onConflictField` is informational only at this layer; the - // caller is responsible for appending any `?on_conflict={field}` - // URL query argument. - val (rewrittenKeys, rewrittenValues) = rewriteForUpsertFallback(headerKeys, headerValues) - val resp = - RunAnywhereBridge.racHttpRequestExecute( - method = method, - url = url, - headerKeys = rewrittenKeys, - headerValues = rewrittenValues, - body = body, - timeoutMs = timeoutMs, - followRedirects = followRedirects, - ) - nativeHttpResponseToResult(resp) - } - internal fun platformDefaultHeaders(): List>? { return try { val flat = RunAnywhereBridge.racHttpDefaultHeaders() ?: return null @@ -589,33 +512,3 @@ private fun nativeHttpResponseToResult(resp: NativeHttpResponse?): HttpExecution ) } } - -/** - * Kotlin-side Supabase upsert rewrite. Commons does not expose an - * upsert-mode HTTP variant, so the Prefer header is rewritten here to - * advertise the `resolution=merge-duplicates` policy expected by - * PostgREST. The URL `?on_conflict={field}` query argument is not - * appended at this layer — the caller owns URL construction. - */ -private fun rewriteForUpsertFallback( - keys: Array, - values: Array, -): Pair, Array> { - val outKeys = keys.copyOf().toMutableList() - val outValues = values.copyOf().toMutableList() - var preferIdx = -1 - for (i in outKeys.indices) { - if (outKeys[i].equals("Prefer", ignoreCase = true)) { - preferIdx = i - break - } - } - val upsertPrefer = "resolution=merge-duplicates,return=representation" - if (preferIdx >= 0) { - outValues[preferIdx] = upsertPrefer - } else { - outKeys.add("Prefer") - outValues.add(upsertPrefer) - } - return outKeys.toTypedArray() to outValues.toTypedArray() -} diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt index f5e94afdae..959170fa73 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeDevice.kt @@ -7,7 +7,7 @@ * Mirrors Swift `CppBridge+Device.swift`: a thin glue layer that wires * a callback bag into the C++ device manager (`rac_device_manager_*`) * and exposes the four registration-state thunks. ALL business logic - * (when to register, dev-vs-prod routing, JSON payload assembly, + * (when to register, endpoint routing, JSON payload assembly, * retry/backoff, last-seen tracking) lives in commons. Kotlin only * provides: * @@ -35,7 +35,6 @@ import ai.runanywhere.proto.v1.DeviceInfo import com.runanywhere.sdk.foundation.bridge.HTTPClientAdapter import com.runanywhere.sdk.foundation.errors.SDKException import com.runanywhere.sdk.native.bridge.RunAnywhereBridge -import com.runanywhere.sdk.public.configuration.SDKEnvironment import kotlinx.coroutines.runBlocking import java.util.Locale @@ -358,19 +357,13 @@ object CppBridgeDevice { } /** - * Mirrors Swift's `UserDefaults` lookup: dev mode always returns - * `false` so commons performs the Supabase UPSERT every launch; - * prod/staging consults the persisted flag so we only send the - * full payload once per installation. + * Mirrors Swift's `UserDefaults` lookup on the registered key: returns + * the persisted flag so commons only sends the full registration + * payload once per installation. Every environment now registers + * through the backend, so there is no dev-mode special case. */ @JvmStatic - fun isDeviceRegisteredCallback(): Boolean { - // Mirrors Swift: development (and pre-init `nil`) always returns false so - // commons performs the Supabase UPSERT every launch. - val env = CppBridgeTelemetry.currentEnvironment - if (env == null || env == SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT) return false - return deviceRegistered - } + fun isDeviceRegisteredCallback(): Boolean = deviceRegistered /** * Mirrors Swift's `UserDefaults.set(_, forKey:)` on the registered @@ -393,10 +386,9 @@ object CppBridgeDevice { * Mirrors Swift's `callbacks.http_post = { ... CppBridge.HTTP.shared.postRaw(...) }`. * * Routes through the canonical [HTTPClientAdapter] — same path used - * by auth and telemetry. The adapter handles base-URL resolution, - * auth header injection, and Supabase upsert detection from the - * endpoint path. The JNI caller is synchronous, so we bridge to the - * adapter's `suspend` API with `runBlocking` (mirrors Swift's + * by auth and telemetry. The adapter handles base-URL resolution and + * auth header injection. The JNI caller is synchronous, so we bridge + * to the adapter's `suspend` API with `runBlocking` (mirrors Swift's * `DispatchSemaphore.wait()` in `CppBridge+Device.swift`). * * @return HTTP status code on response (200/201/409 are success-ish), diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt index 2e0c73358e..0b9f239afd 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt @@ -217,8 +217,7 @@ object CppBridgeDevConfig { object CppBridgeEndpoints { /** Fallback constants used when the native binding is unreachable. * Mirror the canonical values in `rac_endpoints.h`. */ - private const val FALLBACK_DEV_DEVICE_REGISTRATION: String = "/rest/v1/sdk_devices" - private const val FALLBACK_PROD_DEVICE_REGISTRATION: String = "/api/v1/devices/register" + private const val FALLBACK_DEVICE_REGISTRATION: String = "/api/v1/devices/register" private const val FALLBACK_MODEL_ASSIGNMENTS: String = "/api/v1/model-assignments/for-sdk" /** SDK authenticate endpoint. Mirrors Swift's `Endpoints.authenticate`. */ @@ -236,20 +235,13 @@ object CppBridgeEndpoints { /** * Device registration endpoint for [env]. Mirrors Swift's * `Endpoints.deviceRegistration(for:)` — delegates to - * `rac_endpoint_device_registration` via JNI. + * `rac_endpoint_device_registration` via JNI. Every environment now + * registers through the backend (`/api/v1/devices/register`). */ - fun deviceRegistration(env: SDKEnvironment): String { - val fallback = - when (env) { - SDKEnvironment.SDK_ENVIRONMENT_STAGING, - SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION, - -> FALLBACK_PROD_DEVICE_REGISTRATION - else -> FALLBACK_DEV_DEVICE_REGISTRATION - } - return jniOrFallback(fallback) { + fun deviceRegistration(env: SDKEnvironment): String = + jniOrFallback(FALLBACK_DEVICE_REGISTRATION) { RunAnywhereBridge.racEndpointDeviceRegistration(CppBridgeEnvironment.toC(env)) } - } /** Model assignments endpoint. Mirrors Swift's `Endpoints.modelAssignments()`. */ fun modelAssignments(): String = diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt index 0a98f29bf8..94c0525965 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt @@ -1387,10 +1387,6 @@ object RunAnywhereBridge { // `HTTPClientAdapter` to converge on the same canonical SDK header // list and structured API-error parsing Swift consumes, instead of // inlining the policy on the Kotlin side. - // - // Upsert is implemented Kotlin-side in `HTTPClientAdapter.kt` — - // commons does not expose an upsert-mode HTTP variant through the - // flat JNI request signature. /** * Wrapper for `rac_http_default_headers`. Returns commons' canonical From ac36cee55a8649f4015bba9f35344e482ba05450 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:27:36 -0700 Subject: [PATCH 33/44] ios: drop residual Supabase Prefer header for cross-SDK parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 6d8e385a0. The `Prefer: return=representation` header was Supabase PostgREST residue sent on every keyed request; the backend ignores it. Kotlin/Web already dropped it in their upsert cleanup — removing it here keeps iOS (the source of truth) consistent. The `apikey` header is retained. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- .../RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift index 3ca252054d..7d8e2b5c43 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/HTTPClientAdapter.swift @@ -265,8 +265,6 @@ public actor HTTPClientAdapter { headers["X-Platform"] = SDKConstants.platform if let apiKey { headers["apikey"] = apiKey - // Supabase PostgREST: include the inserted/updated row in body. - headers["Prefer"] = "return=representation" } if let authToken { headers["Authorization"] = "Bearer \(authToken)" From c2fa28aaac0e89cced89a37c9fc4eb523caf2e52 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Tue, 21 Jul 2026 14:29:42 -0700 Subject: [PATCH 34/44] web: drop dead /sdk_devices exclusion from smoke-test error filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the upsert cleanup — the Supabase dev device-registration path (/rest/v1/sdk_devices) no longer exists, so whitelisting it in the readiness smoke gate is dead. Removed the obsolete exclusion. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LPk7yo2mZLtKosmMWGnYj4 --- sdk/runanywhere-web/tests/browser/sdk-smoke.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/runanywhere-web/tests/browser/sdk-smoke.spec.ts b/sdk/runanywhere-web/tests/browser/sdk-smoke.spec.ts index fcbe2e0529..ef1aec7c5d 100644 --- a/sdk/runanywhere-web/tests/browser/sdk-smoke.spec.ts +++ b/sdk/runanywhere-web/tests/browser/sdk-smoke.spec.ts @@ -166,8 +166,7 @@ test.describe('Web SDK smoke test', () => { !err.includes('model assignment base URL is not configured') && !err.includes('Device registration requires a matching base URL and API key') && !err.includes('Device registration failed') && - !err.includes('Device registration remained deferred') && - !err.includes('/sdk_devices'), + !err.includes('Device registration remained deferred'), ); expect(fatalErrors, `unexpected console errors:\n${fatalErrors.join('\n')}`).toHaveLength(0); }); From beaf52d8d328bef738775fb49f054737574f923f Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 19 Jul 2026 10:20:20 -0700 Subject: [PATCH 35/44] =?UTF-8?q?feat(rcli):=20control-plane=20network=20d?= =?UTF-8?q?river=20=E2=80=94=20connection=20flags,=20auth,=20telemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns rcli into a control-plane client that can point at any backend for integration testing and manual debugging: - Global --base-url / --api-key / --environment flags (+ RUNANYWHERE_* env vars), threaded into the SDK config with zero regression when absent. - rcli auth login — real handshake (API key -> JWT, device register, model-assignments fetch). - rcli telemetry emit --modality and rcli telemetry blast — model-free telemetry through the real network transport, with a 12-modality result table. Verified end-to-end against a locally-run backend: login registers a device with real hardware facts; blast lands all 12 modalities as verified rows; a bad key exits non-zero. Existing commands unchanged (10/10 CLI tests). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01U4scoVoLn3Z3NxaiK3LC5M --- sdk/runanywhere-cli/CMakeLists.txt | 3 + sdk/runanywhere-cli/README.md | 48 +- sdk/runanywhere-cli/src/app.cpp | 18 + sdk/runanywhere-cli/src/bootstrap.cpp | 183 +++++-- sdk/runanywhere-cli/src/bootstrap.h | 37 ++ sdk/runanywhere-cli/src/commands/cmd_auth.cpp | 122 +++++ .../src/commands/cmd_telemetry.cpp | 478 ++++++++++++++++++ sdk/runanywhere-cli/src/commands/commands.h | 2 + sdk/runanywhere-cli/src/net/control_plane.cpp | 446 ++++++++++++++++ sdk/runanywhere-cli/src/net/control_plane.h | 92 ++++ 10 files changed, 1385 insertions(+), 44 deletions(-) create mode 100644 sdk/runanywhere-cli/src/commands/cmd_auth.cpp create mode 100644 sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp create mode 100644 sdk/runanywhere-cli/src/net/control_plane.cpp create mode 100644 sdk/runanywhere-cli/src/net/control_plane.h diff --git a/sdk/runanywhere-cli/CMakeLists.txt b/sdk/runanywhere-cli/CMakeLists.txt index b64ebd574b..2fb1a107fe 100644 --- a/sdk/runanywhere-cli/CMakeLists.txt +++ b/sdk/runanywhere-cli/CMakeLists.txt @@ -38,6 +38,7 @@ set(RCLI_SOURCES src/catalog/model_ref.cpp src/commands/cmd_version.cpp src/commands/cmd_info.cpp + src/commands/cmd_auth.cpp src/commands/cmd_backends.cpp src/commands/cmd_list.cpp src/commands/cmd_lora.cpp @@ -49,6 +50,7 @@ set(RCLI_SOURCES src/commands/cmd_show.cpp src/commands/cmd_stt.cpp src/commands/cmd_embed.cpp + src/commands/cmd_telemetry.cpp src/commands/cmd_tts.cpp src/commands/cmd_vad.cpp src/commands/cmd_voice.cpp @@ -58,6 +60,7 @@ set(RCLI_SOURCES src/commands/model_setup.cpp src/config/cli_paths.cpp src/device_info.cpp + src/net/control_plane.cpp src/io/wav_io.cpp src/io/image_io.cpp src/io/output.cpp diff --git a/sdk/runanywhere-cli/README.md b/sdk/runanywhere-cli/README.md index 5309e892f9..90d8993493 100644 --- a/sdk/runanywhere-cli/README.md +++ b/sdk/runanywhere-cli/README.md @@ -78,11 +78,57 @@ clear unsupported-backend error. | `rcli serve [model]` | OpenAI-compatible HTTP server (`/v1/chat/completions`, `/v1/models`, `/health`). LLM-only, one model per process | | `rcli backends` | Registered inference backends per primitive | | `rcli info` / `rcli version` | Environment / versions | +| `rcli auth login` | Real control-plane handshake: API key → JWT, device registration, model-assignment fetch | +| `rcli telemetry emit --modality ` | Emit model-free telemetry events of one modality through the real pipeline | +| `rcli telemetry blast` | Emit events of all 12 modalities in one run and print a per-modality result table | Global flags: `--json` (one machine-readable document on stdout), -`--home `, `-v/--verbose`, `-q/--quiet`, `--no-progress`. +`--home `, `-v/--verbose`, `-q/--quiet`, `--no-progress`, plus the +control-plane connection flags below. Exit codes: `0` ok · `1` runtime error · `2` usage error · `130` cancelled. +## Control plane + +rcli can drive any RunAnywhere control plane — including a local backend on +`http://localhost` — with three global flags (each with an env-var fallback): + +| Flag | Env var | Meaning | +|---|---|---| +| `--environment ` | `RUNANYWHERE_ENV` | `dev` (default) is offline — no control plane. `staging` allows `http://` and localhost URLs. `prod` requires `https://` and rejects localhost | +| `--base-url ` | `RUNANYWHERE_BASE_URL` | Backend origin, e.g. `https://api.runanywhere.ai` or `http://127.0.0.1:8000` | +| `--api-key ` | `RUNANYWHERE_API_KEY` | Control-plane API key (≥ 10 chars), required for staging/prod | + +Combos are validated client-side before any network call: staging/prod +require both a key and a URL; passing credentials while in dev mode is an +error. With no flags at all, every command behaves exactly as before +(offline development mode). + +```console +$ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY auth login +organization 293beb67-… +device e87d77a2-… +token expires 2026-07-19T08:44:31Z +device row registered +assignments 0 model(s) + +$ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY telemetry blast +MODALITY RESULT STATUS RECEIVED STORED SKIPPED +llm ok HTTP 200 1 1 0 +… (one row per modality, 12 total) +``` + +- `auth login` runs the same handshake the mobile SDKs run + (`/api/v1/auth/sdk/authenticate` → `/api/v1/devices/register` → + model assignments) and exits non-zero with the server's error surfaced when + anything fails. +- `telemetry emit|blast` drive the real commons telemetry pipeline: payloads + are batched per modality and POSTed to `/api/v2/sdk/telemetry/{modality}` + with the JWT from the login handshake. The V2 endpoints require a JWT, so + both commands log in first — one process performs login + emit (the token + is held in-process, not persisted). Modalities: `llm stt tts vlm rag + imagegen embeddings vad voice lora model system`. Exit is non-zero when any + POST fails or any tracked event never reached the backend. + ### `rcli run` REPL Launched when you give no prompt and stdin is a TTY. Line editing + history diff --git a/sdk/runanywhere-cli/src/app.cpp b/sdk/runanywhere-cli/src/app.cpp index 35e2092c54..9f2704bd13 100644 --- a/sdk/runanywhere-cli/src/app.cpp +++ b/sdk/runanywhere-cli/src/app.cpp @@ -28,6 +28,22 @@ void configure_app(CLI::App& app, GlobalOptions& options) { "RunAnywhere home directory (default: $RUNANYWHERE_HOME or " "~/.local/share/runanywhere; models live under /Models)"); + // Control-plane connection. Absent flags keep the historical offline + // development-mode defaults; validation happens in resolve_connection(). + // RUNANYWHERE_ENV is the CLI11 envname; resolve_connection() also accepts + // RUNANYWHERE_ENVIRONMENT so scripts written for the cross-fit branch work. + app.add_option("--environment", options.environment, + "Control-plane environment: dev (default, offline), staging " + "(keyless OK; http + localhost allowed) or prod (https only)") + ->envname("RUNANYWHERE_ENV") + ->check(CLI::IsMember({"dev", "development", "staging", "prod", "production"})); + app.add_option("--base-url", options.base_url, + "Control-plane base URL, e.g. https://api.runanywhere.ai or " + "http://localhost:8000 (staging/prod)") + ->envname("RUNANYWHERE_BASE_URL"); + app.add_option("--api-key", options.api_key, "Control-plane API key (staging/prod)") + ->envname("RUNANYWHERE_API_KEY"); + commands::register_version(app, options); commands::register_info(app, options); commands::register_backends(app, options); @@ -46,6 +62,8 @@ void configure_app(CLI::App& app, GlobalOptions& options) { commands::register_rag(app, options); commands::register_bench(app, options); commands::register_serve(app, options); + commands::register_auth(app, options); + commands::register_telemetry(app, options); } int run(int argc, char** argv) { diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index 1f5925ffef..bf80831b2a 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -12,6 +12,7 @@ #include "rac/core/rac_core.h" #include "rac/core/rac_logger.h" #include "rac/core/rac_platform_adapter.h" +#include "rac/core/rac_sdk_state.h" #include "rac/desktop/rac_desktop.h" #include "rac/infrastructure/device/rac_device_identity.h" #include "rac/infrastructure/model_management/rac_model_paths.h" @@ -22,7 +23,6 @@ #include "rac/infrastructure/http/rac_http_transport.h" #include "rac/infrastructure/telemetry/rac_telemetry_manager.h" #include "rac/infrastructure/events/rac_sdk_event_stream.h" -#include "rac/core/rac_sdk_state.h" #include "rac/lifecycle/rac_sdk_init.h" #include "rac/foundation/rac_proto_buffer.h" @@ -168,24 +168,35 @@ const char *desktop_platform() { #endif } -rac_environment_t environment_from_name(const std::string &name) { - if (name == "production" || name == "prod") - return RAC_ENV_PRODUCTION; - if (name == "staging") - return RAC_ENV_STAGING; - return RAC_ENV_DEVELOPMENT; +bool parse_environment_name(const std::string &name, rac_environment_t *out) { + if (name.empty() || name == "dev" || name == "development") { + *out = RAC_ENV_DEVELOPMENT; + return true; + } + if (name == "staging") { + *out = RAC_ENV_STAGING; + return true; + } + if (name == "prod" || name == "production") { + *out = RAC_ENV_PRODUCTION; + return true; + } + return false; } ::runanywhere::v1::SdkInitEnvironment -proto_environment_from_name(const std::string &name) { - if (name == "production" || name == "prod") +proto_environment_from_rac(rac_environment_t env) { + switch (env) { + case RAC_ENV_PRODUCTION: return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_PRODUCTION; - if (name == "staging") + case RAC_ENV_STAGING: return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_STAGING; - return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_DEVELOPMENT; + default: + return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_DEVELOPMENT; + } } -void initialize_sdk_metadata() { +void initialize_sdk_metadata(const Connection &connection) { char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; const rac_result_t device_rc = rac_device_get_or_create_persistent_id(device_id, sizeof(device_id)); @@ -198,17 +209,21 @@ void initialize_sdk_metadata() { const std::string locale = detect_locale(); const std::string timezone = detect_timezone(); - const std::string api_key = - first_env_value("RUNANYWHERE_API_KEY", nullptr, nullptr); - const std::string base_url = - first_env_value("RUNANYWHERE_BASE_URL", nullptr, nullptr); - const std::string environment_name = - first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); + // Mirror rac_sdk_init_phase1_proto's step order: runtime state first (the + // auth / device-registration / telemetry paths read env + credentials from + // rac_state), then the copied SDK configuration + client info. + const rac_result_t state_rc = rac_state_initialize( + connection.environment, connection.api_key.c_str(), + connection.base_url.c_str(), device_id[0] != '\0' ? device_id : ""); + if (state_rc != RAC_SUCCESS) { + out::status_line("warning: SDK state init failed: " + + out::describe_result(state_rc)); + } rac_sdk_config_t sdk_config = {}; - sdk_config.environment = environment_from_name(environment_name); - sdk_config.api_key = api_key.c_str(); - sdk_config.base_url = base_url.c_str(); + sdk_config.environment = connection.environment; + sdk_config.api_key = connection.api_key.c_str(); + sdk_config.base_url = connection.base_url.c_str(); sdk_config.device_id = device_id[0] != '\0' ? device_id : ""; sdk_config.platform = desktop_platform(); sdk_config.sdk_version = RCLI_VERSION; @@ -328,24 +343,20 @@ void rcli_telemetry_http_callback(void *user_data, const char *endpoint, // Runs the canonical two-phase SDK init so rcli authenticates and telemetry // actually flushes. Phase 1 sets environment + credentials; Phase 2 // authenticates, registers the device, and enables the telemetry sink. -// Credentials come from the environment (RUNANYWHERE_API_KEY / -// RUNANYWHERE_BASE_URL / RUNANYWHERE_ENVIRONMENT) so no secrets live in source. -// When credentials are absent, rcli stays in local dev mode (no auth, no -// telemetry) exactly as before. -void initialize_telemetry_auth() { - const std::string api_key = - first_env_value("RUNANYWHERE_API_KEY", nullptr, nullptr); - const std::string base_url = - first_env_value("RUNANYWHERE_BASE_URL", nullptr, nullptr); - const std::string environment_name = - first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); +// Connection values come from --environment/--base-url/--api-key (or their +// RUNANYWHERE_* env fallbacks). Development mode stays fully offline. +// Staging allows keyless clients (PUBLIC-org ingestion via baked staging URL). +void initialize_telemetry_auth(const Connection &connection) { + if (connection.environment == RAC_ENV_DEVELOPMENT) { + return; // Local offline mode — no auth, no telemetry. + } // Keyless staging is valid: the baked staging URL resolves in commons and // telemetry flushes unauthenticated (PUBLIC-org ingestion). - const bool staging_keyless = - environment_from_name(environment_name) == RAC_ENV_STAGING; - if ((api_key.empty() || base_url.empty()) && !staging_keyless) { - return; // Local dev mode — telemetry not sent (staging/prod only). + const bool staging_keyless = connection.environment == RAC_ENV_STAGING; + if ((connection.api_key.empty() || connection.base_url.empty()) && + !staging_keyless) { + return; } // Enable the auth manager. NULL secure storage: tokens are not persisted @@ -363,8 +374,8 @@ void initialize_telemetry_auth() { // through rcli_telemetry_http_callback over the desktop HTTP transport; the // terminal batch flushes in rac_shutdown() during teardown. g_telemetry_manager = rac_telemetry_manager_create( - environment_from_name(environment_name), - device_id[0] != '\0' ? device_id : "", desktop_platform(), RCLI_VERSION); + connection.environment, device_id[0] != '\0' ? device_id : "", + desktop_platform(), RCLI_VERSION); if (g_telemetry_manager != nullptr) { rac_telemetry_manager_set_http_callback( g_telemetry_manager, rcli_telemetry_http_callback, g_telemetry_manager); @@ -372,9 +383,9 @@ void initialize_telemetry_auth() { } ::runanywhere::v1::SdkInitPhase1Request phase1; - phase1.set_environment(proto_environment_from_name(environment_name)); - phase1.set_api_key(api_key); - phase1.set_base_url(base_url); + phase1.set_environment(proto_environment_from_rac(connection.environment)); + phase1.set_api_key(connection.api_key); + phase1.set_base_url(connection.base_url); if (device_id[0] != '\0') { phase1.set_device_id(device_id); } @@ -443,6 +454,79 @@ void initialize_telemetry_auth() { } // namespace +rac_result_t resolve_connection(const GlobalOptions &options, Connection *out, + std::string *error) { + // Prefer explicit flags / CLI11 envname (RUNANYWHERE_ENV). Fall back to the + // base-branch alias RUNANYWHERE_ENVIRONMENT so existing scripts keep working. + std::string environment_name = options.environment; + if (environment_name.empty()) { + environment_name = + first_env_value("RUNANYWHERE_ENVIRONMENT", "RUNANYWHERE_ENV", nullptr); + } + std::string base_url = options.base_url; + if (base_url.empty()) { + base_url = first_env_value("RUNANYWHERE_BASE_URL", nullptr, nullptr); + } + std::string api_key = options.api_key; + if (api_key.empty()) { + api_key = first_env_value("RUNANYWHERE_API_KEY", nullptr, nullptr); + } + + Connection connection; + if (!parse_environment_name(environment_name, &connection.environment)) { + if (error) { + *error = "invalid --environment '" + environment_name + + "' (expected dev, staging or prod)"; + } + return RAC_ERROR_INVALID_CONFIGURATION; + } + connection.base_url = std::move(base_url); + connection.api_key = std::move(api_key); + + if (connection.environment == RAC_ENV_DEVELOPMENT) { + if (!connection.api_key.empty() || !connection.base_url.empty()) { + if (error) { + *error = "development mode (the default) has no control plane; pass " + "--environment staging (or prod) together with --base-url " + "and --api-key (staging may omit both for keyless mode)"; + } + return RAC_ERROR_INVALID_CONFIGURATION; + } + if (out) { + *out = connection; + } + return RAC_SUCCESS; + } + + // Staging accepts empty api key + empty URL (baked staging URL / keyless). + // Production still requires a real key + https URL via commons validators. + const rac_validation_result_t key_rc = rac_validate_api_key( + connection.api_key.empty() ? nullptr : connection.api_key.c_str(), + connection.environment); + if (key_rc != RAC_VALIDATION_OK) { + if (error) { + *error = std::string(rac_validation_error_message(key_rc)) + + " (--api-key / RUNANYWHERE_API_KEY)"; + } + return RAC_ERROR_INVALID_CONFIGURATION; + } + const rac_validation_result_t url_rc = rac_validate_base_url( + connection.base_url.empty() ? nullptr : connection.base_url.c_str(), + connection.environment); + if (url_rc != RAC_VALIDATION_OK) { + if (error) { + *error = std::string(rac_validation_error_message(url_rc)) + + " (--base-url / RUNANYWHERE_BASE_URL)"; + } + return RAC_ERROR_INVALID_CONFIGURATION; + } + + if (out) { + *out = connection; + } + return RAC_SUCCESS; +} + rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { const std::string home = paths::resolve_home(options.home_override); if (home.empty()) { @@ -450,6 +534,14 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { return RAC_ERROR_NOT_INITIALIZED; } + Connection connection; + std::string connection_error; + if (resolve_connection(options, &connection, &connection_error) != + RAC_SUCCESS) { + out::error_line(connection_error); + return RAC_ERROR_INVALID_CONFIGURATION; + } + if (!g_bootstrapped) { rac_result_t rc = rac_desktop_adapter_init(nullptr, &g_adapter); if (rc != RAC_SUCCESS) { @@ -490,11 +582,16 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { return rc; } - initialize_sdk_metadata(); + initialize_sdk_metadata(connection); + + // Prefer the richer desktop device-info callbacks from device_info.cpp + // (battery/RAM/CPU/fingerprint). control_plane.cpp still owns login() / + // control_plane_post() for the explicit auth/telemetry commands. if (install_device_callbacks() != RAC_SUCCESS) { out::status_line("warning: device info callbacks failed to register"); } - initialize_telemetry_auth(); + + initialize_telemetry_auth(connection); #if defined(RCLI_HAS_LLAMACPP) if (rac_backend_llamacpp_register() != RAC_SUCCESS) { diff --git a/sdk/runanywhere-cli/src/bootstrap.h b/sdk/runanywhere-cli/src/bootstrap.h index eb24e012aa..65f5c84f56 100644 --- a/sdk/runanywhere-cli/src/bootstrap.h +++ b/sdk/runanywhere-cli/src/bootstrap.h @@ -17,6 +17,7 @@ #include #include "rac/core/rac_types.h" +#include "rac/infrastructure/network/rac_environment.h" typedef struct rac_telemetry_manager rac_telemetry_manager_t; @@ -29,8 +30,44 @@ struct GlobalOptions { bool quiet = false; bool no_progress = false; std::string home_override; // --home flag + + // Control-plane connection. Empty defaults preserve the historical + // offline development-mode behavior exactly. CLI11 fills these from + // --base-url/--api-key/--environment with RUNANYWHERE_BASE_URL / + // RUNANYWHERE_API_KEY / RUNANYWHERE_ENV env-var fallbacks (app.cpp). + // resolve_connection() also accepts RUNANYWHERE_ENVIRONMENT as an alias + // for the environment name (keyless staging remains valid). + std::string environment; // dev|development|staging|prod|production ("" → dev) + std::string base_url; // staging may omit (baked URL); prod requires https + std::string api_key; // staging may omit (keyless); prod requires ≥10 chars +}; + +/** + * Validated control-plane connection resolved from GlobalOptions. + * bootstrap() threads these values into rac_state / rac_sdk_config so the + * commons auth, device-registration, and telemetry paths can read them. + */ +struct Connection { + rac_environment_t environment = RAC_ENV_DEVELOPMENT; + std::string base_url; + std::string api_key; }; +/** + * Resolve + validate the connection flags client-side (before any network + * call). On failure fills `error` with an actionable message and returns + * RAC_ERROR_INVALID_CONFIGURATION. + * + * Rules (mirrors commons rac_validate_api_key / rac_validate_base_url): + * - dev (default): no credentials allowed — pass --environment staging to + * target a real control plane (localhost is allowed on staging). + * - staging: keyless OK (baked staging URL / PUBLIC-org); optional key+URL. + * - prod: api key + https base URL required; localhost rejected. + * + * Env aliases: RUNANYWHERE_ENV (CLI11) and RUNANYWHERE_ENVIRONMENT (fallback). + */ +rac_result_t resolve_connection(const GlobalOptions& options, Connection* out, std::string* error); + /** Resolved environment after bootstrap. */ struct Bootstrapped { std::string home; // RunAnywhere home (storage base dir) diff --git a/sdk/runanywhere-cli/src/commands/cmd_auth.cpp b/sdk/runanywhere-cli/src/commands/cmd_auth.cpp new file mode 100644 index 0000000000..33e7bd1a2b --- /dev/null +++ b/sdk/runanywhere-cli/src/commands/cmd_auth.cpp @@ -0,0 +1,122 @@ +/** + * @file cmd_auth.cpp + * @brief `rcli auth login` — real control-plane handshake. + * + * Runs the canonical staging/production auth sequence against the configured + * backend (--base-url/--api-key/--environment or their RUNANYWHERE_* env + * vars): authenticate (API key → JWT + refresh token), device registration, + * and model-assignment fetch — all through commons entry points + * (net::login → rac_auth_* + rac_sdk_init_phase2_proto). + */ + +#include "commands/commands.h" + +#include +#include +#include + +#include "net/control_plane.h" + +#include "io/output.h" + +namespace rcli::commands { + +namespace { + +std::string format_epoch_seconds(int64_t seconds) { + if (seconds <= 0) { + return "-"; + } + const time_t secs = static_cast(seconds); + struct tm tm_info{}; +#if defined(_WIN32) + if (gmtime_s(&tm_info, &secs) != 0) { + return std::to_string(seconds); + } +#else + if (gmtime_r(&secs, &tm_info) == nullptr) { + return std::to_string(seconds); + } +#endif + char buffer[32] = {}; + strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &tm_info); + return buffer; +} + +int run_auth_login(const GlobalOptions& options) { + Bootstrapped env; + if (bootstrap(options, &env) != RAC_SUCCESS) { + return 1; + } + + net::LoginSummary summary; + std::string error; + if (net::login(&summary, &error) != RAC_SUCCESS) { + out::error_line(error); + return 1; + } + + // A staging/production login without a device row is a broken control + // plane — surface it as a failure, not a footnote. + const bool ok = summary.device_registered; + + if (options.json) { + out::JsonWriter json; + json.begin_object() + .field("success", ok) + .field("organization_id", summary.organization_id) + .field("user_id", summary.user_id) + .field("device_id", summary.backend_device_id) + .field("device_uuid", summary.persistent_device_id) + .field("token_expires_at", format_epoch_seconds(summary.token_expires_at)) + .field("device_registered", summary.device_registered) + .field("assignments", static_cast(summary.assignment_count)); + if (!summary.warning.empty()) { + json.field("warning", summary.warning); + } + json.end_object(); + out::result_line(json.str()); + } else { + out::result_line("organization " + summary.organization_id); + out::result_line("user " + + (summary.user_id.empty() ? std::string("-") : summary.user_id)); + out::result_line("device " + summary.backend_device_id); + out::result_line("device-uuid " + summary.persistent_device_id); + out::result_line("token expires " + format_epoch_seconds(summary.token_expires_at)); + out::result_line(std::string("device row ") + + (summary.device_registered ? "registered" : "NOT registered")); + out::result_line("assignments " + std::to_string(summary.assignment_count) + + " model(s)"); + if (!summary.warning.empty()) { + out::status_line("warning: " + summary.warning); + } + } + + if (!ok) { + out::error_line("device registration did not complete" + + (summary.warning.empty() ? "" : ": " + summary.warning)); + return 1; + } + return 0; +} + +} // namespace + +void register_auth(CLI::App& app, GlobalOptions& options) { + CLI::App* cmd = app.add_subcommand("auth", "Control-plane authentication"); + cmd->require_subcommand(1); + + CLI::App* login_cmd = cmd->add_subcommand( + "login", + "Authenticate against the configured backend (API key → JWT), register " + "this device and fetch model assignments. Requires --environment " + "staging|prod with --base-url and --api-key (or RUNANYWHERE_* env vars)."); + login_cmd->callback([&options]() { + const int exit_code = run_auth_login(options); + if (exit_code != 0) { + throw CLI::RuntimeError(exit_code); + } + }); +} + +} // namespace rcli::commands diff --git a/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp b/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp new file mode 100644 index 0000000000..e2f6c3d418 --- /dev/null +++ b/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp @@ -0,0 +1,478 @@ +/** + * @file cmd_telemetry.cpp + * @brief `rcli telemetry emit|blast` — model-free control-plane telemetry. + * + * Drives the real commons telemetry pipeline end-to-end: payloads are queued + * with rac_telemetry_manager_track, batched + serialized by commons + * (one POST per modality to /api/v2/sdk/telemetry/{modality}), and delivered + * through the CLI's HTTP callback over the registered curl transport with the + * JWT from the login handshake. + * + * Staging/production only (the V2 endpoints require a JWT); both commands run + * the login handshake first, so one process does login + emit. Exits non-zero + * when any POST fails or any tracked event never reached the backend. + */ + +#include "commands/commands.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rac/core/rac_platform_adapter.h" +#include "rac/core/rac_sdk_state.h" +#include "rac/infrastructure/telemetry/rac_telemetry_manager.h" +#include "rac/infrastructure/telemetry/rac_telemetry_types.h" + +#include "io/output.h" +#include "net/control_plane.h" + +#ifndef RCLI_VERSION +#define RCLI_VERSION "0.0.0-dev" +#endif + +namespace rcli::commands { + +namespace { + +// The 12 modalities recognized by the V2 telemetry pipeline (one backend +// endpoint each), paired with a realistic terminal event type drawn from the +// canonical names the SDK emits (telemetry_manager.cpp / the backend's +// normalizer treats *.completed as terminal). +struct ModalitySpec { + const char* name; + const char* default_event_type; +}; + +constexpr ModalitySpec kModalities[] = { + {"llm", "llm.generation.completed"}, + {"stt", "stt.transcription.completed"}, + {"tts", "tts.synthesis.completed"}, + {"vlm", "vlm.process.completed"}, + {"rag", "rag.query.completed"}, + {"imagegen", "imagegen.generate.completed"}, + {"embeddings", "embeddings.embed.completed"}, + {"vad", "vad.stopped"}, + {"voice", "voice.turn.metrics"}, + {"lora", "lora.attach.completed"}, + {"model", "model.download.completed"}, + {"system", "sdk.init.completed"}, +}; + +const ModalitySpec* find_modality(const std::string& name) { + for (const ModalitySpec& spec : kModalities) { + if (name == spec.name) { + return &spec; + } + } + return nullptr; +} + +std::vector modality_names() { + std::vector names; + for (const ModalitySpec& spec : kModalities) { + names.emplace_back(spec.name); + } + return names; +} + +std::string uuid4() { + static thread_local std::mt19937_64 rng{std::random_device{}()}; + std::uniform_int_distribution dist; + uint64_t hi = dist(rng); + uint64_t lo = dist(rng); + hi = (hi & 0xFFFFFFFFFFFF0FFFull) | 0x0000000000004000ull; // version 4 + lo = (lo & 0x3FFFFFFFFFFFFFFFull) | 0x8000000000000000ull; // RFC-4122 variant + char buffer[37] = {}; + std::snprintf(buffer, sizeof(buffer), "%08" PRIx64 "-%04" PRIx64 "-%04" PRIx64 "-%04" PRIx64 + "-%012" PRIx64, + hi >> 32, (hi >> 16) & 0xFFFFull, hi & 0xFFFFull, lo >> 48, + lo & 0xFFFFFFFFFFFFull); + return buffer; +} + +// Minimal field extraction from the backend's SDKTelemetryBatchResponse JSON +// ({"success":true,"events_received":N,"events_stored":N,"events_skipped":N, +// "storage_version":"V2"}). The CLI deliberately carries no JSON parser. +int extract_int_field(const std::string& json, const std::string& key) { + const std::string needle = "\"" + key + "\":"; + const size_t pos = json.find(needle); + if (pos == std::string::npos) { + return -1; + } + return std::atoi(json.c_str() + pos + needle.size()); +} + +bool extract_bool_field(const std::string& json, const std::string& key) { + const std::string needle = "\"" + key + "\":"; + const size_t pos = json.find(needle); + return pos != std::string::npos && json.compare(pos + needle.size(), 4, "true") == 0; +} + +// Per-endpoint accounting accumulated inside the telemetry HTTP callback. +struct EndpointStats { + int posts = 0; + int failures = 0; + int last_status = 0; + int received = 0; + int stored = 0; + int skipped = 0; + std::string last_error; +}; + +struct TelemetryHttpContext { + std::map endpoints; // key: endpoint path +}; + +void telemetry_http_callback(void* user_data, const char* endpoint, const char* json_body, + size_t json_length, rac_bool_t requires_auth) { + auto* context = static_cast(user_data); + if (context == nullptr || endpoint == nullptr) { + return; + } + const net::HttpResult result = net::control_plane_post( + endpoint, std::string(json_body != nullptr ? json_body : "", json_length), + requires_auth == RAC_TRUE); + + EndpointStats& stats = context->endpoints[endpoint]; + stats.posts += 1; + stats.last_status = result.status; + if (!result.ok()) { + stats.failures += 1; + stats.last_error = result.describe(); + return; + } + if (!extract_bool_field(result.body, "success")) { + stats.failures += 1; + stats.last_error = "backend reported success=false: " + result.body; + } + const int received = extract_int_field(result.body, "events_received"); + const int stored = extract_int_field(result.body, "events_stored"); + const int skipped = extract_int_field(result.body, "events_skipped"); + stats.received += received > 0 ? received : 0; + stats.stored += stored > 0 ? stored : 0; + stats.skipped += skipped > 0 ? skipped : 0; +} + +/** Optional metric flags shared by emit and blast. Negative = unset. */ +struct MetricOptions { + double processing_ms = -1.0; + int32_t input_tokens = -1; + int32_t output_tokens = -1; + double audio_duration_ms = -1.0; +}; + +void track_events(rac_telemetry_manager_t* manager, const ModalitySpec& spec, + const std::string& event_type, const std::string& session_id, int count, + const MetricOptions& metrics) { + for (int i = 0; i < count; ++i) { + const std::string event_id = uuid4(); + rac_telemetry_payload_t payload = rac_telemetry_payload_default(); + payload.id = event_id.c_str(); + payload.event_type = event_type.c_str(); + payload.modality = spec.name; + payload.session_id = session_id.c_str(); + const int64_t now_ms = rac_get_current_time_ms(); + payload.timestamp_ms = now_ms; + payload.created_at_ms = now_ms; + payload.success = RAC_TRUE; + payload.has_success = RAC_TRUE; + if (metrics.processing_ms >= 0) { + payload.processing_time_ms = metrics.processing_ms; + payload.has_processing_time_ms = RAC_TRUE; + } + if (metrics.input_tokens >= 0) { + payload.input_tokens = metrics.input_tokens; + } + if (metrics.output_tokens >= 0) { + payload.output_tokens = metrics.output_tokens; + payload.total_tokens = (metrics.input_tokens > 0 ? metrics.input_tokens : 0) + + metrics.output_tokens; + } + if (metrics.audio_duration_ms >= 0) { + payload.audio_duration_ms = metrics.audio_duration_ms; + } + rac_telemetry_manager_track(manager, &payload); + } +} + +struct FlushReport { + TelemetryHttpContext context; + int tracked = 0; +}; + +/** + * Login (JWT), create a manager wired to the real transport, run `track_fn`, + * flush, and account per-endpoint results. Returns false on login failure. + */ +template +bool run_telemetry_session(const GlobalOptions& options, FlushReport* report, TrackFn&& track_fn) { + Bootstrapped env; + if (bootstrap(options, &env) != RAC_SUCCESS) { + return false; + } + + // The V2 telemetry endpoints only accept a JWT, so emit implies login — + // one process performs the handshake and the flush (in-process token). + std::string error; + if (net::login(nullptr, &error) != RAC_SUCCESS) { + out::error_line(error); + return false; + } + + const char* device_id = rac_state_get_device_id(); + rac_telemetry_manager_t* manager = rac_telemetry_manager_create( + rac_state_get_environment(), device_id != nullptr ? device_id : "", net::platform_name(), + RCLI_VERSION); + if (manager == nullptr) { + out::error_line("telemetry manager creation failed"); + return false; + } + rac_telemetry_manager_set_device_info(manager, net::device_model().c_str(), + net::os_version_string().c_str()); + rac_telemetry_manager_set_http_callback(manager, telemetry_http_callback, &report->context); + + report->tracked = track_fn(manager); + rac_telemetry_manager_flush(manager); + rac_telemetry_manager_set_http_callback(manager, nullptr, nullptr); + rac_telemetry_manager_destroy(manager); + return true; +} + +int total_received(const FlushReport& report) { + int received = 0; + for (const auto& [endpoint, stats] : report.context.endpoints) { + received += stats.received; + } + return received; +} + +bool report_failed(const FlushReport& report) { + if (report.context.endpoints.empty()) { + return true; // nothing was POSTed — flush deferred or dropped + } + for (const auto& [endpoint, stats] : report.context.endpoints) { + if (stats.failures > 0) { + return true; + } + } + return total_received(report) != report.tracked; +} + +void render_endpoint_results(const GlobalOptions& options, const FlushReport& report) { + if (options.json) { + out::JsonWriter json; + json.begin_object() + .field("tracked", static_cast(report.tracked)) + .field("success", !report_failed(report)) + .begin_array("endpoints"); + for (const auto& [endpoint, stats] : report.context.endpoints) { + json.begin_array_object() + .field("endpoint", endpoint) + .field("posts", static_cast(stats.posts)) + .field("http_status", static_cast(stats.last_status)) + .field("events_received", static_cast(stats.received)) + .field("events_stored", static_cast(stats.stored)) + .field("events_skipped", static_cast(stats.skipped)); + if (!stats.last_error.empty()) { + json.field("error", stats.last_error); + } + json.end_object(); + } + json.end_array().end_object(); + out::result_line(json.str()); + return; + } + + if (report.context.endpoints.empty()) { + out::error_line("no telemetry batch was sent (flush deferred?)"); + return; + } + for (const auto& [endpoint, stats] : report.context.endpoints) { + std::string line = endpoint + " HTTP " + std::to_string(stats.last_status) + + " received=" + std::to_string(stats.received) + + " stored=" + std::to_string(stats.stored) + + " skipped=" + std::to_string(stats.skipped); + if (!stats.last_error.empty()) { + line += " error: " + stats.last_error; + } + out::result_line(line); + } +} + +int run_telemetry_emit(const GlobalOptions& options, const std::string& modality, + const std::string& event_type, int count, const std::string& session_id, + const MetricOptions& metrics) { + const ModalitySpec* spec = find_modality(modality); + if (spec == nullptr) { + out::error_line("unknown modality '" + modality + "'"); + return 2; + } + const std::string resolved_event_type = + event_type.empty() ? spec->default_event_type : event_type; + const std::string resolved_session = session_id.empty() ? uuid4() : session_id; + + FlushReport report; + const bool session_ok = run_telemetry_session( + options, &report, [&](rac_telemetry_manager_t* manager) { + track_events(manager, *spec, resolved_event_type, resolved_session, count, metrics); + return count; + }); + if (!session_ok) { + return 1; + } + + if (!options.json) { + out::status_line("emitted " + std::to_string(count) + " × " + resolved_event_type + + " (modality " + modality + ", session " + resolved_session + ")"); + } + render_endpoint_results(options, report); + return report_failed(report) ? 1 : 0; +} + +int run_telemetry_blast(const GlobalOptions& options, int count, const std::string& session_id, + const MetricOptions& metrics) { + const std::string resolved_session = session_id.empty() ? uuid4() : session_id; + + FlushReport report; + const bool session_ok = run_telemetry_session( + options, &report, [&](rac_telemetry_manager_t* manager) { + for (const ModalitySpec& spec : kModalities) { + track_events(manager, spec, spec.default_event_type, resolved_session, count, + metrics); + } + return count * static_cast(std::size(kModalities)); + }); + if (!session_ok) { + return 1; + } + + bool all_ok = true; + std::vector> rows; + for (const ModalitySpec& spec : kModalities) { + const std::string endpoint = std::string("/api/v2/sdk/telemetry/") + spec.name; + const auto it = report.context.endpoints.find(endpoint); + std::string status = "NO POST"; + int received = 0; + int stored = 0; + int skipped = 0; + bool row_ok = false; + if (it != report.context.endpoints.end()) { + const EndpointStats& stats = it->second; + received = stats.received; + stored = stats.stored; + skipped = stats.skipped; + row_ok = stats.failures == 0 && stats.received == count; + status = row_ok ? ("HTTP " + std::to_string(stats.last_status)) + : (stats.last_error.empty() + ? "HTTP " + std::to_string(stats.last_status) + : stats.last_error); + } + all_ok = all_ok && row_ok; + rows.push_back({spec.name, row_ok ? "ok" : "FAILED", status, std::to_string(received), + std::to_string(stored), std::to_string(skipped)}); + } + + if (options.json) { + out::JsonWriter json; + json.begin_object() + .field("tracked", static_cast(report.tracked)) + .field("success", all_ok) + .field("session_id", resolved_session) + .begin_array("modalities"); + for (const auto& row : rows) { + json.begin_array_object() + .field("modality", row[0]) + .field("ok", row[1] == "ok") + .field("status", row[2]) + .field("events_received", static_cast(std::atoi(row[3].c_str()))) + .field("events_stored", static_cast(std::atoi(row[4].c_str()))) + .field("events_skipped", static_cast(std::atoi(row[5].c_str()))) + .end_object(); + } + json.end_array().end_object(); + out::result_line(json.str()); + } else { + out::status_line("blast session " + resolved_session + " — " + + std::to_string(report.tracked) + " event(s) across " + + std::to_string(std::size(kModalities)) + " modalities"); + out::table({"MODALITY", "RESULT", "STATUS", "RECEIVED", "STORED", "SKIPPED"}, rows); + } + return all_ok ? 0 : 1; +} + +} // namespace + +void register_telemetry(CLI::App& app, GlobalOptions& options) { + CLI::App* cmd = app.add_subcommand( + "telemetry", "Emit model-free telemetry through the real control-plane pipeline"); + cmd->require_subcommand(1); + + // ---- telemetry emit ---------------------------------------------------- + CLI::App* emit_cmd = cmd->add_subcommand( + "emit", + "Track N events of one modality, flush to /api/v2/sdk/telemetry/{modality} " + "and report the backend's accounting. Runs the auth handshake first " + "(staging/prod only). Exits non-zero when any POST fails."); + auto modality = std::make_shared(); + auto event_type = std::make_shared(); + auto count = std::make_shared(1); + auto session_id = std::make_shared(); + auto metrics = std::make_shared(); + emit_cmd->add_option("--modality", *modality, "Telemetry modality") + ->required() + ->check(CLI::IsMember(modality_names())); + emit_cmd->add_option("--event-type", *event_type, + "Event type string (default: the modality's terminal event, e.g. " + "llm.generation.completed)"); + emit_cmd->add_option("--count", *count, "Number of events to emit (default 1)") + ->check(CLI::PositiveNumber); + emit_cmd->add_option("--session-id", *session_id, + "Session id attached to every event (default: fresh UUID)"); + emit_cmd->add_option("--processing-ms", metrics->processing_ms, + "processing_time_ms metric for every event"); + emit_cmd->add_option("--input-tokens", metrics->input_tokens, + "input_tokens metric (llm/vlm modalities)"); + emit_cmd->add_option("--output-tokens", metrics->output_tokens, + "output_tokens metric (llm/vlm modalities)"); + emit_cmd->add_option("--audio-duration-ms", metrics->audio_duration_ms, + "audio_duration_ms metric (stt modality)"); + emit_cmd->callback([&options, modality, event_type, count, session_id, metrics]() { + const int exit_code = run_telemetry_emit(options, *modality, *event_type, *count, + *session_id, *metrics); + if (exit_code != 0) { + throw CLI::RuntimeError(exit_code); + } + }); + + // ---- telemetry blast --------------------------------------------------- + CLI::App* blast_cmd = cmd->add_subcommand( + "blast", + "Emit --count events of EVERY modality (all 12) in one run, flush, and " + "print a per-modality result table parsed from the backend's batch " + "responses. Emits one event of every modality."); + auto blast_count = std::make_shared(1); + auto blast_session = std::make_shared(); + auto blast_metrics = std::make_shared(); + blast_cmd->add_option("--count", *blast_count, "Events per modality (default 1)") + ->check(CLI::PositiveNumber); + blast_cmd->add_option("--session-id", *blast_session, + "Session id attached to every event (default: fresh UUID)"); + blast_cmd->add_option("--processing-ms", blast_metrics->processing_ms, + "processing_time_ms metric for every event"); + blast_cmd->callback([&options, blast_count, blast_session, blast_metrics]() { + const int exit_code = + run_telemetry_blast(options, *blast_count, *blast_session, *blast_metrics); + if (exit_code != 0) { + throw CLI::RuntimeError(exit_code); + } + }); +} + +} // namespace rcli::commands diff --git a/sdk/runanywhere-cli/src/commands/commands.h b/sdk/runanywhere-cli/src/commands/commands.h index 473f85ac98..b89f5edae2 100644 --- a/sdk/runanywhere-cli/src/commands/commands.h +++ b/sdk/runanywhere-cli/src/commands/commands.h @@ -38,6 +38,8 @@ void register_serve(CLI::App& app, GlobalOptions& options); void register_lora(CLI::App& app, GlobalOptions& options); void register_rag(CLI::App& app, GlobalOptions& options); void register_bench(CLI::App& app, GlobalOptions& options); +void register_auth(CLI::App& app, GlobalOptions& options); +void register_telemetry(CLI::App& app, GlobalOptions& options); /** * Shared pull flow (plan → start → progress → terminal state) for an diff --git a/sdk/runanywhere-cli/src/net/control_plane.cpp b/sdk/runanywhere-cli/src/net/control_plane.cpp new file mode 100644 index 0000000000..d74f0d410f --- /dev/null +++ b/sdk/runanywhere-cli/src/net/control_plane.cpp @@ -0,0 +1,446 @@ +/** + * @file control_plane.cpp + * @brief Control-plane network wiring for rcli — see control_plane.h. + * + * The CLI supplies platform callbacks (device info + HTTP via the registered + * curl transport) and drives the canonical commons entry points. Request + * building (rac_auth_build_authenticate_request, device registration JSON) + * and response parsing (rac_auth_handle_authenticate_response, + * SdkInitResult) stay in commons per the repo layering rule. + */ + +#include "net/control_plane.h" + +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif +#if !defined(_WIN32) +#include +#include +#endif + +#include "rac/core/rac_platform_adapter.h" +#include "rac/core/rac_sdk_state.h" +#include "rac/infrastructure/device/rac_device_manager.h" +#include "rac/infrastructure/http/rac_http_client.h" +#include "rac/infrastructure/network/rac_auth_manager.h" +#include "rac/infrastructure/network/rac_endpoints.h" +#include "rac/infrastructure/network/rac_environment.h" +#include "rac/lifecycle/rac_sdk_init.h" + +#include "sdk_init.pb.h" + +#include "io/output.h" +#include "io/proto.h" + +namespace rcli::net { + +namespace { + +namespace v1 = runanywhere::v1; + +constexpr size_t kErrorBodyPreview = 500; + +std::string single_line_preview(const std::string& body) { + std::string preview = body.substr(0, kErrorBodyPreview); + for (char& ch : preview) { + if (ch == '\n' || ch == '\r' || ch == '\t') { + ch = ' '; + } + } + if (body.size() > kErrorBodyPreview) { + preview += "…"; + } + return preview; +} + +std::string query_hostname() { +#if defined(_WIN32) + const char* name = std::getenv("COMPUTERNAME"); + return name != nullptr ? name : "windows-host"; +#else + struct utsname info{}; + if (uname(&info) == 0 && info.nodename[0] != '\0') { + return info.nodename; + } + return "desktop-host"; +#endif +} + +std::string query_device_model() { +#if defined(__APPLE__) + char model[128] = {}; + size_t size = sizeof(model); + if (sysctlbyname("hw.model", model, &size, nullptr, 0) == 0 && model[0] != '\0') { + return model; + } + return "Mac"; +#elif defined(_WIN32) + return "Windows PC"; +#else + struct utsname info{}; + if (uname(&info) == 0 && info.machine[0] != '\0') { + return std::string(info.sysname[0] != '\0' ? info.sysname : "Linux") + " " + info.machine; + } + return "Linux PC"; +#endif +} + +std::string query_os_version() { +#if defined(_WIN32) + return {}; +#else + struct utsname info{}; + if (uname(&info) == 0 && info.release[0] != '\0') { + // Backend os_version column caps at 20 chars. + return std::string(info.release).substr(0, 20); + } + return {}; +#endif +} + +std::string query_chip_name() { +#if defined(__APPLE__) + char brand[256] = {}; + size_t size = sizeof(brand); + if (sysctlbyname("machdep.cpu.brand_string", brand, &size, nullptr, 0) == 0 && + brand[0] != '\0') { + return brand; + } +#endif + return {}; +} + +const char* architecture_name() { +#if defined(__aarch64__) || defined(_M_ARM64) + return "arm64"; +#else + return "x86_64"; +#endif +} + +// --------------------------------------------------------------------------- +// Device-manager callbacks. The device manager reads the strings we hand it +// after the callback returns (it builds the registration JSON immediately), +// so all backing storage is file-static — the CLI drives one control-plane +// flow at a time. +// --------------------------------------------------------------------------- + +struct DeviceBridgeState { + bool registered_this_process = false; + std::string device_id; // rac_state persistent UUID snapshot + std::string device_name; // hostname + std::string response_body; // outlives the http_post callback + std::string response_error; // outlives the http_post callback +}; + +DeviceBridgeState& device_state() { + static DeviceBridgeState state; + return state; +} + +void device_get_info(rac_device_registration_info_t* out_info, void* /*user_data*/) { + if (out_info == nullptr) { + return; + } + DeviceBridgeState& state = device_state(); + state.device_name = query_hostname(); + + *out_info = {}; + out_info->device_model = device_model().c_str(); + out_info->device_name = state.device_name.c_str(); + out_info->platform = platform_name(); + out_info->os_version = os_version_string().c_str(); + out_info->form_factor = "desktop"; + out_info->architecture = architecture_name(); + static const std::string chip = query_chip_name(); + out_info->chip_name = chip.c_str(); + + rac_memory_info_t memory{}; + const rac_platform_adapter_t* adapter = rac_get_platform_adapter(); + if (adapter != nullptr && adapter->get_memory_info != nullptr && + adapter->get_memory_info(&memory, adapter->user_data) == RAC_SUCCESS) { + out_info->total_memory = static_cast(memory.total_bytes); + out_info->available_memory = static_cast(memory.available_bytes); + } + + out_info->has_neural_engine = RAC_FALSE; + out_info->neural_engine_cores = 0; +#if defined(__APPLE__) + out_info->gpu_family = "apple"; +#else + out_info->gpu_family = nullptr; +#endif + out_info->battery_level = -1.0; // desktop: unavailable → null on the wire + out_info->battery_state = nullptr; + out_info->is_low_power_mode = RAC_FALSE; + out_info->core_count = static_cast(std::thread::hardware_concurrency()); + out_info->performance_cores = 0; + out_info->efficiency_cores = 0; + out_info->device_fingerprint = nullptr; // commons falls back to device_id +} + +const char* device_get_id(void* /*user_data*/) { + DeviceBridgeState& state = device_state(); + const char* device_id = rac_state_get_device_id(); + state.device_id = device_id != nullptr ? device_id : ""; + return state.device_id.c_str(); +} + +rac_bool_t device_is_registered(void* /*user_data*/) { + return device_state().registered_this_process ? RAC_TRUE : RAC_FALSE; +} + +void device_set_registered(rac_bool_t registered, void* /*user_data*/) { + device_state().registered_this_process = (registered == RAC_TRUE); +} + +rac_result_t device_http_post(const char* endpoint, const char* json_body, + rac_bool_t requires_auth, rac_device_http_response_t* out_response, + void* /*user_data*/) { + if (endpoint == nullptr || json_body == nullptr || out_response == nullptr) { + return RAC_ERROR_INVALID_ARGUMENT; + } + DeviceBridgeState& state = device_state(); + const HttpResult result = control_plane_post(endpoint, json_body, requires_auth == RAC_TRUE); + state.response_body = result.body; + state.response_error = result.ok() ? std::string() : result.describe(); + + *out_response = {}; + out_response->status_code = result.status; + out_response->response_body = state.response_body.empty() ? nullptr + : state.response_body.c_str(); + if (result.ok()) { + out_response->result = RAC_SUCCESS; + return RAC_SUCCESS; + } + out_response->result = + result.transport != RAC_SUCCESS ? result.transport : RAC_ERROR_HTTP_ERROR; + out_response->error_message = state.response_error.c_str(); + return out_response->result; +} + +} // namespace + +const char* platform_name() { +#if defined(__APPLE__) + return "macos"; +#elif defined(__linux__) + return "linux"; +#elif defined(_WIN32) + return "windows"; +#else + return "desktop"; +#endif +} + +const std::string& device_model() { + static const std::string model = query_device_model(); + return model; +} + +const std::string& os_version_string() { + static const std::string version = query_os_version(); + return version; +} + +void register_device_callbacks() { + rac_device_callbacks_t callbacks = {}; + callbacks.get_device_info = device_get_info; + callbacks.get_device_id = device_get_id; + callbacks.is_registered = device_is_registered; + callbacks.set_registered = device_set_registered; + callbacks.http_post = device_http_post; + callbacks.user_data = nullptr; + if (rac_device_manager_set_callbacks(&callbacks) != RAC_SUCCESS) { + out::status_line("warning: device manager callbacks failed to install"); + } +} + +std::string HttpResult::describe() const { + if (transport != RAC_SUCCESS) { + std::string message = "network error: " + out::describe_result(transport); + if (!body.empty()) { + message += " (" + single_line_preview(body) + ")"; + } + return message; + } + std::string message = "HTTP " + std::to_string(status); + if (!body.empty()) { + message += ": " + single_line_preview(body); + } + return message; +} + +HttpResult control_plane_post(const std::string& endpoint, const std::string& json_body, + bool bearer_auth) { + HttpResult result; + + const char* base_url = rac_state_get_base_url(); + if (base_url == nullptr || base_url[0] == '\0') { + result.transport = RAC_ERROR_INVALID_CONFIGURATION; + result.body = "control-plane base URL is not configured"; + return result; + } + + char url[2048] = {}; + if (rac_build_url(base_url, endpoint.c_str(), url, sizeof(url)) < 0) { + result.transport = RAC_ERROR_INVALID_CONFIGURATION; + result.body = "failed to build control-plane URL"; + return result; + } + + // Canonical control-plane header set — mirrors commons' phase-2 pattern: + // defaults (Content-Type/Accept/X-SDK-*) + X-Platform + apikey [+ Bearer]. + const rac_http_header_kv_t* defaults = nullptr; + size_t default_count = 0; + std::vector headers; + if (rac_http_default_headers(&defaults, &default_count) == RAC_SUCCESS && + defaults != nullptr) { + headers.assign(defaults, defaults + default_count); + } + headers.push_back({"X-Platform", platform_name()}); + const char* api_key = rac_state_get_api_key(); + if (api_key != nullptr && api_key[0] != '\0') { + headers.push_back({"apikey", api_key}); + } + std::string bearer; + if (bearer_auth) { + const char* token = rac_auth_get_access_token(); + if (token != nullptr && token[0] != '\0') { + bearer = std::string("Bearer ") + token; + headers.push_back({"Authorization", bearer.c_str()}); + } + } + + rac_http_client_t* client = nullptr; + rac_result_t rc = rac_http_client_create(&client); + if (rc != RAC_SUCCESS) { + result.transport = rc; + return result; + } + + rac_http_request_t request = {}; + request.method = "POST"; + request.url = url; + request.headers = headers.data(); + request.header_count = headers.size(); + request.body_bytes = reinterpret_cast(json_body.data()); + request.body_len = json_body.size(); + request.timeout_ms = rac_env_default_http_timeout_ms(rac_state_get_environment()); + // Credential-bearing control-plane requests never replay across redirects. + request.follow_redirects = RAC_FALSE; + + rac_http_response_t response = {}; + rc = rac_http_request_send(client, &request, &response); + rac_http_client_destroy(client); + + result.transport = rc; + if (rc == RAC_SUCCESS) { + result.status = response.status; + if (response.body_bytes != nullptr && response.body_len > 0) { + result.body.assign(reinterpret_cast(response.body_bytes), + response.body_len); + } + } + rac_http_response_free(&response); + return result; +} + +rac_result_t login(LoginSummary* out, std::string* error) { + const rac_environment_t env = rac_state_get_environment(); + if (!rac_env_requires_auth(env)) { + if (error != nullptr) { + *error = + "development mode (the default) has no control plane; pass " + "--environment staging (or prod) together with --base-url and --api-key"; + } + return RAC_ERROR_INVALID_CONFIGURATION; + } + + // Step 1: API key → JWT. Idempotent within a process; a valid token + // short-circuits (phase 2 below then takes its authenticated fast path). + if (!rac_auth_is_authenticated() || rac_auth_needs_refresh()) { + const rac_sdk_config_t* config = rac_sdk_get_config(); + if (config == nullptr) { + if (error != nullptr) { + *error = "SDK configuration unavailable (bootstrap did not run?)"; + } + return RAC_ERROR_NOT_INITIALIZED; + } + char* request_json = rac_auth_build_authenticate_request(config); + if (request_json == nullptr) { + if (error != nullptr) { + *error = "failed to build authenticate request"; + } + return RAC_ERROR_INVALID_CONFIGURATION; + } + const HttpResult response = + control_plane_post(RAC_ENDPOINT_AUTHENTICATE, request_json, false); + std::free(request_json); + if (!response.ok()) { + if (error != nullptr) { + *error = "authentication failed: " + response.describe(); + } + return response.transport != RAC_SUCCESS ? response.transport : RAC_ERROR_HTTP_ERROR; + } + const int auth_rc = rac_auth_handle_authenticate_response(response.body.c_str()); + if (auth_rc != RAC_SUCCESS && auth_rc != RAC_ERROR_SECURE_STORAGE_FAILED) { + if (error != nullptr) { + *error = "authentication response rejected: " + single_line_preview(response.body); + } + return RAC_ERROR_INVALID_RESPONSE; + } + } + + // Step 2: canonical phase-2 orchestration — device registration + + // model-assignment fetch (telemetry flush / local rescans stay off; the + // CLI runs those flows through their own commands). + v1::SdkInitPhase2Request request; + const std::string request_bytes = proto::serialize(request); + rac_proto_buffer_t out_buffer; + rac_proto_buffer_init(&out_buffer); + const rac_result_t phase2_rc = rac_sdk_init_phase2_proto( + request_bytes.empty() ? nullptr + : reinterpret_cast(request_bytes.data()), + request_bytes.size(), &out_buffer); + v1::SdkInitResult result; + std::string parse_error; + if (!proto::parse_proto_buffer(&out_buffer, &result, &parse_error) || + phase2_rc != RAC_SUCCESS) { + if (error != nullptr) { + *error = "services init failed: " + + (parse_error.empty() ? out::describe_result(phase2_rc) : parse_error); + } + return phase2_rc != RAC_SUCCESS ? phase2_rc : RAC_ERROR_INVALID_RESPONSE; + } + if (!result.success()) { + if (error != nullptr) { + *error = "services init failed: " + result.error().message(); + } + return RAC_ERROR_INVALID_STATE; + } + + if (out != nullptr) { + const char* organization_id = rac_auth_get_organization_id(); + const char* user_id = rac_auth_get_user_id(); + const char* backend_device_id = rac_auth_get_device_id(); + const char* persistent_device_id = rac_state_get_device_id(); + out->organization_id = organization_id != nullptr ? organization_id : ""; + out->user_id = user_id != nullptr ? user_id : ""; + out->backend_device_id = backend_device_id != nullptr ? backend_device_id : ""; + out->persistent_device_id = persistent_device_id != nullptr ? persistent_device_id : ""; + out->token_expires_at = rac_auth_get_token_expires_at(); + out->device_registered = result.device_registered(); + out->assignment_count = result.linked_models_count(); + out->warning = result.warning(); + } + return RAC_SUCCESS; +} + +} // namespace rcli::net diff --git a/sdk/runanywhere-cli/src/net/control_plane.h b/sdk/runanywhere-cli/src/net/control_plane.h new file mode 100644 index 0000000000..ac2dafce26 --- /dev/null +++ b/sdk/runanywhere-cli/src/net/control_plane.h @@ -0,0 +1,92 @@ +/** + * @file control_plane.h + * @brief Control-plane network wiring for rcli (auth, device, telemetry HTTP). + * + * rcli is the 6th consumer of runanywhere-commons and plays the same role the + * Swift/Kotlin/Flutter/RN/Web bridges play for the control plane: it supplies + * the platform-side callbacks (device info, persistent device id, HTTP POST) + * and drives the canonical commons entry points + * (rac_auth_* + rac_sdk_init_phase2_proto). All handshake sequencing, JSON + * request building, and response parsing stay in commons. + * + * Requires bootstrap() (rac_init + curl transport + rac_state) to have run. + */ + +#ifndef RCLI_NET_CONTROL_PLANE_H +#define RCLI_NET_CONTROL_PLANE_H + +#include +#include + +#include "rac/core/rac_types.h" + +namespace rcli::net { + +/** "macos" / "linux" / "windows" — the X-Platform header + auth payload value. */ +const char* platform_name(); + +/** Best-effort local hardware model (e.g. "Mac16,8"); empty when unknown. */ +const std::string& device_model(); + +/** Best-effort OS version string (kernel release); empty when unknown. */ +const std::string& os_version_string(); + +/** + * Install the CLI's rac_device_callbacks_t: device info gathered from the + * desktop platform adapter, the rac_state persistent device id, an in-process + * registration flag, and an HTTP POST that routes through the registered curl + * transport (Bearer token attached when the request requires auth). + * Idempotent; called from bootstrap(). + */ +void register_device_callbacks(); + +/** One buffered control-plane HTTP exchange. */ +struct HttpResult { + rac_result_t transport = RAC_SUCCESS; ///< send-level result (network/TLS/timeout) + int32_t status = 0; ///< HTTP status (0 when transport failed) + std::string body; ///< response body (server error JSON on 4xx/5xx) + + [[nodiscard]] bool ok() const { + return transport == RAC_SUCCESS && status >= 200 && status < 300; + } + /** "HTTP 401: {...}" / "network error" — for user-facing error lines. */ + [[nodiscard]] std::string describe() const; +}; + +/** + * POST `endpoint` (path, e.g. "/api/v2/sdk/telemetry/llm") against the + * configured base URL with the canonical control-plane headers + * (commons defaults + X-Platform + apikey). When `bearer_auth` is true the + * current JWT access token is attached as `Authorization: Bearer `. + */ +HttpResult control_plane_post(const std::string& endpoint, const std::string& json_body, + bool bearer_auth); + +/** Result of the real auth handshake (authenticate → device → assignments). */ +struct LoginSummary { + std::string organization_id; + std::string user_id; // may be empty (org-scoped keys) + std::string backend_device_id; // control-plane device row id (auth response) + std::string persistent_device_id; // SDK persistent UUID (device fingerprint) + int64_t token_expires_at = 0; // unix seconds + bool device_registered = false; + uint32_t assignment_count = 0; + std::string warning; // non-fatal phase-2 notes +}; + +/** + * Run the real control-plane handshake against the configured backend: + * 1. POST /api/v1/auth/sdk/authenticate (API key → JWT + refresh token), + * 2. rac_sdk_init_phase2_proto (device registration + model-assignment + * fetch through the commons lifecycle orchestrator). + * + * Requires a staging/production environment (development mode has no control + * plane). Idempotent within a process — a valid token short-circuits step 1. + * On failure returns a non-SUCCESS code and fills `error` with the + * server-surfaced message (HTTP status + response body). + */ +rac_result_t login(LoginSummary* out, std::string* error); + +} // namespace rcli::net + +#endif // RCLI_NET_CONTROL_PLANE_H From ee90038cec5c4938283ee0e99aa5a458974cb078 Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 19 Jul 2026 10:20:52 -0700 Subject: [PATCH 36/44] examples: align all five apps to brand orange #FF6900 + design guideline Every example app had drifted from the brand: UI accents were the legacy #FF5500 (Flutter/RN were plain blue) while the logos were already the correct #FF6900. This finishes the migration. - examples/DESIGN_GUIDELINE.md: canonical brand doc (primary = the logo's #FF6900, gradient ->#FB2C36, full palette light+dark, per-platform mapping, the white-on-orange contrast caveat). - iOS (AppColors + AccentColor + Keyboard/Activity extensions), Android (Color.kt tonal ramp on the brand hue; dead template swatches removed), Flutter (mislabeled-blue primary -> brand, seed pinned), Web (CSS tokens + loading SVG + theme-color), React Native (two competing theme systems consolidated onto one anchored to #FF6900; legacy barrel deleted; migrated screens now get dark mode). - Each app's AGENTS.md gains a Design System section pointing at the guideline (CLAUDE.md symlinks intact); parent AGENTS.md references it. - Removed the dead rac_telemetry_manager_parse_response declaration + export. Grep-proven: zero legacy accent literals remain; #FF6900 present in all five. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01U4scoVoLn3Z3NxaiK3LC5M --- AGENTS.md | 2 + examples/DESIGN_GUIDELINE.md | 155 +++++ examples/android/RunAnywhereAI/AGENTS.md | 10 + .../screens/benchmark/BenchmarkShareCard.kt | 2 +- .../runanywhereai/ui/theme/Color.kt | 18 +- .../app/src/main/res/values/colors.xml | 9 +- examples/flutter/RunAnywhereAI/AGENTS.md | 10 + .../RunAnywhereAI/lib/app/content_view.dart | 2 +- .../lib/app/runanywhere_ai_app.dart | 10 +- .../lib/core/design_system/app_colors.dart | 10 +- examples/ios/RunAnywhereAI/AGENTS.md | 4 +- .../AccentColor.colorset/Contents.json | 2 +- .../Core/DesignSystem/AppColors.swift | 12 +- .../VoiceKeyboard/FlowActivationView.swift | 2 +- ...nywhereActivityExtensionLiveActivity.swift | 4 +- .../RunAnywhereKeyboard/KeyboardView.swift | 4 +- examples/react-native/RunAnywhereAI/AGENTS.md | 17 +- examples/react-native/RunAnywhereAI/App.tsx | 139 ++-- .../src/components/chat/LoRASheet.tsx | 341 +++++---- .../src/components/chat/ToolCallIndicator.tsx | 270 ++++---- .../src/components/chat/TypingIndicator.tsx | 72 +- .../src/components/common/LoadingOverlay.tsx | 111 +-- .../components/common/ModelStatusBanner.tsx | 214 +++--- .../src/navigation/BottomTabs.tsx | 35 +- .../src/screens/ChatAnalyticsScreen.tsx | 648 +++++++++--------- .../RunAnywhereAI/src/screens/ChatScreen.tsx | 185 +++-- .../src/screens/StorageScreen.tsx | 295 ++++---- .../RunAnywhereAI/src/screens/VLMScreen.tsx | 432 ++++++------ .../RunAnywhereAI/src/theme/colors.ts | 106 --- .../RunAnywhereAI/src/theme/index.ts | 53 -- .../RunAnywhereAI/src/theme/spacing.ts | 135 ---- .../RunAnywhereAI/src/theme/system/colors.ts | 65 +- .../RunAnywhereAI/src/theme/system/index.ts | 1 + .../src/theme/system/themedStyles.ts | 35 + .../RunAnywhereAI/src/theme/typography.ts | 157 ----- .../RunAnywhereAI/src/utils/modelDisplay.ts | 26 +- examples/web/RunAnywhereAI/AGENTS.md | 13 + examples/web/RunAnywhereAI/index.html | 2 +- examples/web/RunAnywhereAI/src/main.ts | 4 +- .../RunAnywhereAI/src/styles/components.css | 10 +- .../src/styles/design-system.css | 38 +- .../exports/RACommons.exports | 1 - .../telemetry/rac_telemetry_manager.h | 10 - 43 files changed, 1745 insertions(+), 1926 deletions(-) create mode 100644 examples/DESIGN_GUIDELINE.md delete mode 100644 examples/react-native/RunAnywhereAI/src/theme/colors.ts delete mode 100644 examples/react-native/RunAnywhereAI/src/theme/index.ts delete mode 100644 examples/react-native/RunAnywhereAI/src/theme/spacing.ts create mode 100644 examples/react-native/RunAnywhereAI/src/theme/system/themedStyles.ts delete mode 100644 examples/react-native/RunAnywhereAI/src/theme/typography.ts diff --git a/AGENTS.md b/AGENTS.md index 1adbeba69e..f8999bc5d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,8 @@ Cross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-comm | React Native | `examples/react-native/RunAnywhereAI/` | RN 0.85 + NitroModules | | Web | `examples/web/RunAnywhereAI/` | Vanilla TS + Vite | +All example apps share one visual identity — brand orange `#FF6900` (the logo primary, **not** the legacy `#FF5500`), documented in `examples/DESIGN_GUIDELINE.md`. Each app hand-maintains a small theme file that mirrors that doc; see the "Design System" section in each app's `AGENTS.md`. + ### Playground `Playground/` contains 6 standalone demo projects (not part of any build system): YapRun (iOS dictation app), swift-starter-app, on-device-browser-agent, android-use-agent, linux-voice-assistant, openclaw-hybrid-assistant. diff --git a/examples/DESIGN_GUIDELINE.md b/examples/DESIGN_GUIDELINE.md new file mode 100644 index 0000000000..f05ab65b2c --- /dev/null +++ b/examples/DESIGN_GUIDELINE.md @@ -0,0 +1,155 @@ +# RunAnywhere Design Guideline + +Canonical visual identity for the five RunAnywhere example apps in this folder +(iOS, Android, Flutter, React Native, Web). One brand, one palette, one type system. + +> **Source of truth.** This document is the canonical brand reference for the +> example apps. Each app hand-maintains a small set of theme constants that +> **mirror the values here** (Swift/Kotlin/Dart/RN can't share a single stylesheet), +> and each theme file carries a header comment pointing back to this doc. When the +> brand values change, update this document and the per-app theme files together. + +--- + +## 1. Brand color — the primary is the logo + +The RunAnywhere mark is a two-path gradient. Its **start stop is the brand primary**: + +| Role | Hex | HSL | Notes | +|------|-----|-----|-------| +| **Primary (brand orange)** | `#FF6900` | `hsl(24.7, 100%, 50%)` | The logo's gradient start. This is THE brand color. | +| Gradient end | `#FB2C36` | `hsl(357.3, 96%, 58%)` | The logo's gradient end (red). `#FB2D36` is the 1/255 HSL-rounded spelling used by the token CSS; `#FB2C36` is the raw-SVG spelling — either is acceptable. | +| Brand gradient | `linear-gradient(135°, #FF6900 → #FB2C36)` | — | Used for the mark, hero CTAs, brand moments. | + +**Do not use the legacy `#FF5500` orange-red, `#FF9500` (Apple orange), or any blue as +the accent.** Every app previously drifted to one of those; the logos are already +`#FF6900` — the theme must match the logo. + +Supporting brand neutrals: + +| Role | Hex | HSL | +|------|-----|-----| +| Ink (foreground text) | `#10182B` | `hsl(220, 40%, 11%)` | +| Paper (light background) | `#FBFAF8` | `hsl(40, 20%, 98%)` | +| Surface inverse (dark background) | `#0C0E17` | `hsl(229, 31%, 7%)` | +| Surface inverse elevated | `#1B2231` | `hsl(224, 30%, 15%)` | + +--- + +## 2. Full semantic palette (light / dark) + +HSL triplets are authoritative; hex is the native mirror. Where one value is given, the +token is theme-invariant. + +| Token | Light | Dark | +|-------|-------|------| +| `background` | `#FBFAF8` (`40 20% 98%`) | `#0C0E17` (`229 31% 7%`) | +| `foreground` | `#10182B` (`220 40% 11%`) | `#F7F4EE` (`40 25% 96%`) | +| `card` / surface | `#FBFAF8` | `#131620` (`228 26% 10%`) | +| `card-foreground` | `#10182B` | `#F7F4EE` | +| `muted` / secondary | `#F3F4F6` (`220 14% 96%`) | `#1C2230` (`228 22% 13%`) | +| `muted-foreground` | `#6B7280` (`220 9% 46%`) | `#9AA1B3` (`227 14% 66%`) | +| `border` / input | `#E5E7EB` (`220 13% 91%`) | `#242A38` (`228 18% 17%`) | +| **`primary`** | `#FF6900` | `#FF6900` | +| `primary-foreground` | `#FFFFFF` | `#FFFFFF` — but see §5 contrast | +| `ring` / focus | `#FF6900` | `#FF6900` | +| `destructive` / `error` | `#EF4444` (`0 84% 60%`) | `#DC2626` (`0 72% 51%`) | +| `success` | `#269B57` (`145 60% 38%`) | `#45C97F` (`145 50% 52%`) | +| `warning` | `#F59E0B` (`38 92% 50%`) | `#F7AE2A` (`38 92% 55%`) | +| `info` | `#3B82F6` (`217 91% 60%`) | `#60A5FA` (`213 94% 68%`) | +| `code-surface` (theme-invariant) | `#021A28` (`207 95% 8%`) | — | +| `code-foreground` | `#D3DCE8` (`217 34% 88%`) | — | + +`radius`: **8px** (`0.5rem`). Focus ring: 2px `#FF6900` (offset by the background color). + +--- + +## 3. Typography + +| Role | Family | Fallback | +|------|--------|----------| +| Display / headings-as-brand-moment | **Instrument Serif** | Georgia, serif | +| Body / UI | **IBM Plex Sans** | system-ui, sans-serif | +| Code / metrics / mono | **JetBrains Mono** | ui-monospace, monospace | + +Fonts are a **target**, not a hard requirement for every example app today. Apps that +already ship system fonts (iOS uses SF; several apps use Figtree) may keep them for now +and adopt the brand fonts as a follow-up — the **color palette is the priority**. When +adopting brand fonts, bundle the woff2/ttf from Google Fonts (all three are +OFL-licensed) and reserve Instrument Serif for display only. + +--- + +## 4. Per-platform mapping + +Every app defines these in its ONE theme file (cite this doc in that file's header). + +### SwiftUI (iOS) +`Core/DesignSystem/AppColors.swift` + `Assets.xcassets/AccentColor.colorset`. +```swift +static let primary = Color(hex: 0xFF6900) // brand orange — was 0xFF5500 +static let gradientEnd = Color(hex: 0xFB2C36) +static let backgroundDark = Color(hex: 0x0C0E17) // brand ink surface +static let backgroundLight = Color(hex: 0xFBFAF8) // paper +// AccentColor.colorset components → R 0xFF, G 0x69, B 0x00 +``` +The brand gradient: `LinearGradient(colors: [primary, gradientEnd], startPoint: .topLeading, endPoint: .bottomTrailing)`. + +### Jetpack Compose (Android) +`ui/theme/Color.kt` + `Theme.kt` (Material 3 `lightColorScheme`/`darkColorScheme`). +```kotlin +val BrandOrange = Color(0xFFFF6900) // was 0xFFFF5500 +val BrandGradientEnd = Color(0xFFFB2C36) +// map BrandOrange → primary in both schemes; regenerate the Primary tonal ramp around this hue +``` +`success`/`warning`/`info` have no Material 3 role — expose them via an extended-colors `CompositionLocal`. Brand gradient via `Brush.linearGradient(listOf(BrandOrange, BrandGradientEnd))`. + +### Flutter +`lib/core/design_system/app_colors.dart` + the two `ThemeData` blocks in the app root. +```dart +static const Color primary = Color(0xFFFF6900); // was Colors.blue +static const Color gradientEnd = Color(0xFFFB2C36); +// ColorScheme.fromSeed(seedColor: primary).copyWith(primary: primary) — pin exact primary +``` +Add success/warning/info via a `ThemeExtension`. Brand gradient via `LinearGradient(colors:[primary, gradientEnd], begin: Alignment.topLeft, end: Alignment.bottomRight)`. + +### React Native +`src/theme/system/colors.ts` (the Material-3 scheme — the canonical one; the legacy `src/theme/colors.ts` blue system is being retired). +```ts +export const brand = { primary: '#FF6900', gradientEnd: '#FB2C36' } // primary was #E65500 / legacy #007AFF +// anchor lightScheme.primary and darkScheme.primary to '#FF6900' +``` +Brand gradient via `expo-linear-gradient` `colors={['#FF6900', '#FB2C36']}`. Keep token keys 1:1 with the CSS var names so web + native share one vocabulary. + +### Web / CSS +`src/styles/design-system.css` — CSS custom properties. +```css +--color-primary: #FF6900; /* was #FF5500 */ +--color-primary-strong: #E65E00; +--gradient-brand: linear-gradient(135deg, #FF6900 0%, #FB2C36 100%); +``` + +--- + +## 5. Contrast — the one honest caveat + +**White text on solid `#FF6900` is ≈2.9:1 and FAILS WCAG AA.** Ink (`#10182B`) on +`#FF6900` is ≈6.1:1 and passes comfortably. The brand accepts white-on-orange for the +gradient CTA and large/bold brand moments (a documented, deliberate deviation), but: + +- **Reserve solid-orange fills with white text for large or bold text only.** +- For small text on orange, or any body copy, use **ink text on orange**, or use orange + as a border / accent / icon color instead of a text-bearing fill. +- **Never** put orange fill behind white body copy. +- Do **not** darken `#FF6900` to "fix" contrast — the hue is the locked brand identity. + +--- + +## 6. Rules + +1. **`#FF6900` is the primary everywhere.** No `#FF5500`, `#FF9500`, `#007AFF`, or `Colors.blue` as the accent. +2. **One theme file per app**, mirroring §2/§4, with a header comment citing this doc. +3. **Both light and dark** must be defined; dark backgrounds trend toward brand ink `#0C0E17`, light toward paper `#FBFAF8` (approximate is fine; exact is better). +4. **Logos are already on-brand** (`#FF6900 → #FB2C36`) — never repaint the mark; only the UI theme was lagging. +5. Third-party brand marks (Meta, Mistral, HuggingFace, macOS traffic lights, syntax highlighting) are intentionally off-palette — leave them. +6. When in doubt, use the exact values in this document — it is the reference. diff --git a/examples/android/RunAnywhereAI/AGENTS.md b/examples/android/RunAnywhereAI/AGENTS.md index 445857390e..f247661ea6 100644 --- a/examples/android/RunAnywhereAI/AGENTS.md +++ b/examples/android/RunAnywhereAI/AGENTS.md @@ -31,3 +31,13 @@ Private QHexRT device and Play-release orchestration lives in the sibling checko After editing these scripts, run `bash -n scripts/*.sh`, `bash scripts/sync-solutions-yamls.sh --check`, `scripts/smoke.sh`, and `git diff --check`. + +## Design System + +Brand primary is RunAnywhere orange **#FF6900** (the logo color) — see the canonical +`../../DESIGN_GUIDELINE.md`. Theming is 100% Jetpack Compose Material 3: +`app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Color.kt` (`BrandOrange = +0xFFFF6900` + the `Primary*` tonal ramp around the #FF6900 hue) and `Theme.kt` +(`lightColorScheme`/`darkColorScheme`, no dynamic color so the brand is guaranteed). +`res/values/colors.xml` holds only structural black/white. When changing brand colors, +edit `Color.kt` and keep it in sync with the guideline. diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/benchmark/BenchmarkShareCard.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/benchmark/BenchmarkShareCard.kt index 3dab7de07a..9d5e293e0c 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/benchmark/BenchmarkShareCard.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/benchmark/BenchmarkShareCard.kt @@ -64,7 +64,7 @@ import kotlin.coroutines.cancellation.CancellationException // same regardless of the app's active Material color scheme). private val CardBackgroundTop = Color(0xFF1A0E06) private val CardBackgroundBottom = Color(0xFF0B0B0C) -private val BrandOrange = Color(0xFFFF5500) +private val BrandOrange = Color(0xFFFF6900) // RunAnywhere brand orange (see examples/DESIGN_GUIDELINE.md) private val CardTextPrimary = Color(0xFFF5F3F1) private val CardTextSecondary = Color(0xFF9A938E) private val CardRowBackground = Color(0x14FFFFFF) diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Color.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Color.kt index 89f7ce27f2..dc33df235e 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Color.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/Color.kt @@ -2,17 +2,17 @@ package com.runanywhere.runanywhereai.ui.theme import androidx.compose.ui.graphics.Color -// Primary tonal — Orange -val Primary20 = Color(0xFF4E1C00) -val Primary30 = Color(0xFF732B00) -val Primary60 = Color(0xFFE65500) -val Primary70 = Color(0xFFFF6D1F) -val Primary80 = Color(0xFFFFB693) -val Primary90 = Color(0xFFFFDBCA) +// Primary tonal — RunAnywhere brand orange (#FF6900 hue); see examples/DESIGN_GUIDELINE.md +val Primary20 = Color(0xFF4C1F00) +val Primary30 = Color(0xFF732F00) +val Primary60 = Color(0xFFE65E00) +val Primary70 = Color(0xFFFF7B1F) +val Primary80 = Color(0xFFFFC094) +val Primary90 = Color(0xFFFFE1CC) -// Canonical brand accent shared with the iOS and web examples (#FF5500). +// Canonical brand accent — the RunAnywhere logo orange (#FF6900), shared with every example app. // Used as the dark-scheme primary so brand moments match across platforms. -val BrandOrange = Color(0xFFFF5500) +val BrandOrange = Color(0xFFFF6900) // Secondary tonal — Warm Neutral val Secondary10 = Color(0xFF1F1A17) diff --git a/examples/android/RunAnywhereAI/app/src/main/res/values/colors.xml b/examples/android/RunAnywhereAI/app/src/main/res/values/colors.xml index f8c6127d32..cd843feb84 100644 --- a/examples/android/RunAnywhereAI/app/src/main/res/values/colors.xml +++ b/examples/android/RunAnywhereAI/app/src/main/res/values/colors.xml @@ -1,10 +1,7 @@ + - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 #FF000000 #FFFFFFFF - \ No newline at end of file + diff --git a/examples/flutter/RunAnywhereAI/AGENTS.md b/examples/flutter/RunAnywhereAI/AGENTS.md index 390fa192bb..9afb3638b8 100644 --- a/examples/flutter/RunAnywhereAI/AGENTS.md +++ b/examples/flutter/RunAnywhereAI/AGENTS.md @@ -6,6 +6,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co A Flutter reference app demonstrating the RunAnywhere on-device AI SDK. It mirrors the native iOS app's feature set: LLM chat (streaming + non-streaming), speech-to-text, text-to-speech, voice assistant pipeline (STT→LLM→TTS), vision/VLM with live camera, tool calling, RAG with PDF ingestion, structured JSON output, and a solutions YAML runner. Eight tabs: Chat, Vision, STT, Speak, Voice, Tools, Solutions, Settings. +## Design System + +Brand primary is RunAnywhere orange **#FF6900** (the logo color) — see the canonical +`../../DESIGN_GUIDELINE.md`. Theme lives in `lib/core/design_system/app_colors.dart` +(`AppColors.brandOrange`/`primaryAccent`) and the two `ThemeData` blocks in +`lib/app/runanywhere_ai_app.dart` (Material 3, seed = brand orange with `primary` +pinned exactly, light + dark via `ThemeMode.system`). `primaryBlue` is a genuine +secondary blue, not the brand. When changing brand colors, edit `app_colors.dart` and +keep it in sync with the guideline. + ## Common Commands ```bash diff --git a/examples/flutter/RunAnywhereAI/lib/app/content_view.dart b/examples/flutter/RunAnywhereAI/lib/app/content_view.dart index 7f3a4edd88..f3c13ad407 100644 --- a/examples/flutter/RunAnywhereAI/lib/app/content_view.dart +++ b/examples/flutter/RunAnywhereAI/lib/app/content_view.dart @@ -61,7 +61,7 @@ class _ContentViewState extends State { // accessibility tap-target bounds match the visible icon centres // and the "Transcribe" label doesn't truncate. height: 80, - indicatorColor: AppColors.primaryBlue.withValues(alpha: 0.2), + indicatorColor: AppColors.primaryAccent.withValues(alpha: 0.2), onDestinationSelected: (index) { setState(() { _selectedTab = index; diff --git a/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart b/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart index 8febe32fd8..cea80bb24c 100644 --- a/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart +++ b/examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart @@ -221,20 +221,20 @@ class _RunAnywhereAIAppState extends State { debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primaryBlue, + seedColor: AppColors.brandOrange, brightness: Brightness.light, - ), + ).copyWith(primary: AppColors.brandOrange), useMaterial3: true, appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0), navigationBarTheme: NavigationBarThemeData( - indicatorColor: AppColors.primaryBlue.withValues(alpha: 0.2), + indicatorColor: AppColors.primaryAccent.withValues(alpha: 0.2), ), ), darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primaryBlue, + seedColor: AppColors.brandOrange, brightness: Brightness.dark, - ), + ).copyWith(primary: AppColors.brandOrange), useMaterial3: true, appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0), ), diff --git a/examples/flutter/RunAnywhereAI/lib/core/design_system/app_colors.dart b/examples/flutter/RunAnywhereAI/lib/core/design_system/app_colors.dart index d95f9481b2..b67e062cc8 100644 --- a/examples/flutter/RunAnywhereAI/lib/core/design_system/app_colors.dart +++ b/examples/flutter/RunAnywhereAI/lib/core/design_system/app_colors.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; -/// App Colors (mirroring iOS AppColors.swift) +/// App Colors (mirroring iOS AppColors.swift). +/// Brand primary is #FF6900 — see examples/DESIGN_GUIDELINE.md (the canonical palette). class AppColors { // MARK: - Semantic Colors - static Color get primaryAccent => Colors.blue; - static const Color primaryBlue = Colors.blue; + /// RunAnywhere brand orange — the logo primary (was Colors.blue). + static const Color brandOrange = Color(0xFFFF6900); + static const Color gradientEnd = Color(0xFFFB2C36); + static Color get primaryAccent => brandOrange; + static const Color primaryBlue = Colors.blue; // genuine secondary blue accent static const Color primaryGreen = Colors.green; static const Color primaryRed = Colors.red; static const Color primaryOrange = Colors.orange; diff --git a/examples/ios/RunAnywhereAI/AGENTS.md b/examples/ios/RunAnywhereAI/AGENTS.md index 6b078dd4a6..67e15483ac 100644 --- a/examples/ios/RunAnywhereAI/AGENTS.md +++ b/examples/ios/RunAnywhereAI/AGENTS.md @@ -296,7 +296,7 @@ RunAnywhereAI/ │ └── ContentView.swift # 5-tab navigation shell ├── Core/ │ ├── DesignSystem/ -│ │ ├── AppColors.swift # Brand colors (primary: #FF5500) +│ │ ├── AppColors.swift # Brand colors (primary: #FF6900) │ │ ├── AppSpacing.swift # Layout constants + AppLayout namespace │ │ ├── Typography.swift # Font constants (AppTypography) │ │ └── ViewCompatibility.swift # Cross-platform nav shims @@ -553,7 +553,7 @@ existing canonical proto APIs, it can live in `RunAnywhere+ExampleShims.swift`. ## Design System All styling is centralized — no inline magic numbers or color literals in views: -- **Colors**: `AppColors` — brand primary `#FF5500`, semantic tokens for text/backgrounds/bubbles/badges/status +- **Colors**: `AppColors` — brand primary `#FF6900` (the RunAnywhere logo orange), semantic tokens for text/backgrounds/bubbles/badges/status. Canonical palette: `../../DESIGN_GUIDELINE.md`. - **Spacing**: `AppSpacing` — xxSmall(2) to xxxLarge(40), icon sizes, button heights, corner radii, strokes - **Typography**: `AppTypography` — system text styles + custom sizes + weighted/monospaced variants - **Layout**: `AppLayout` — window sizes, content widths, animation durations diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/ios/RunAnywhereAI/RunAnywhereAI/Assets.xcassets/AccentColor.colorset/Contents.json index f298c6c2c4..9f439d0d10 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -6,7 +6,7 @@ "components" : { "alpha" : "1.000", "blue" : "0x00", - "green" : "0x55", + "green" : "0x69", "red" : "0xFF" } }, diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Core/DesignSystem/AppColors.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Core/DesignSystem/AppColors.swift index 768cfd1e58..35d797a1b5 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Core/DesignSystem/AppColors.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Core/DesignSystem/AppColors.swift @@ -4,8 +4,8 @@ // // RunAnywhere Brand Color Palette // Color scheme matching RunAnywhere.ai website -// Primary accent: Vibrant orange-red (#FF5500) - matches website branding -// Dark theme backgrounds: Deep dark blue-gray matching website aesthetic +// Primary accent: RunAnywhere brand orange (#FF6900) - the logo primary +// Brand tokens mirror examples/DESIGN_GUIDELINE.md (canonical source) // import SwiftUI @@ -34,8 +34,8 @@ struct AppColors { // PRIMARY ACCENT COLORS - RunAnywhere Brand Colors // ==================== // Primary brand color - vibrant orange/red from RunAnywhere.ai website - static let primaryAccent = Color(hex: 0xFF5500) // Vibrant orange-red - primary brand color - static let primaryOrange = Color(hex: 0xFF5500) // Same as primary accent + static let primaryAccent = Color(hex: 0xFF6900) // RunAnywhere brand orange - the logo primary + static let primaryOrange = Color(hex: 0xFF6900) // Same as primary accent static let primaryBlue = Color(hex: 0x3B82F6) // Blue-500 - for secondary elements static let primaryGreen = Color(hex: 0x10B981) // Emerald-500 - success green static let primaryRed = Color(hex: 0xEF4444) // Red-500 - error red @@ -96,7 +96,7 @@ struct AppColors { // ==================== // User bubbles (with gradient support) - using vibrant orange/red brand color static let userBubbleGradientStart = primaryAccent // Vibrant orange-red - static let userBubbleGradientEnd = Color(hex: 0xE64500) // Slightly darker orange-red + static let userBubbleGradientEnd = Color(hex: 0xE65E00) // Slightly darker brand orange static let messageBubbleUser = primaryAccent // Vibrant orange-red // Assistant bubbles - clean gray (uses system colors for dark mode adaptation) @@ -106,7 +106,7 @@ struct AppColors { static let messageBubbleAssistantGradientEnd = backgroundGray6 // Dark mode - toned down variant for reduced eye strain in low-light - static let messageBubbleUserDark = Color(hex: 0xCC4400) // Darker orange-red (80% brightness) + static let messageBubbleUserDark = Color(hex: 0xCC5400) // Darker brand orange (80% brightness) static let messageBubbleAssistantDark = backgroundGray5Dark // Dark gray // ==================== diff --git a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/VoiceKeyboard/FlowActivationView.swift b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/VoiceKeyboard/FlowActivationView.swift index fc0ce9b66b..531c8e7d1d 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/VoiceKeyboard/FlowActivationView.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereAI/Features/VoiceKeyboard/FlowActivationView.swift @@ -7,7 +7,7 @@ // // Purpose: Start the background AVAudioSession and instruct the user to swipe // back to the host app. Dismisses automatically once the session is ready. -// Branded with RunAnywhere color palette (#FF5500 primary accent). +// Branded with RunAnywhere color palette (#FF6900 primary accent). // #if os(iOS) diff --git a/examples/ios/RunAnywhereAI/RunAnywhereActivityExtension/RunAnywhereActivityExtensionLiveActivity.swift b/examples/ios/RunAnywhereAI/RunAnywhereActivityExtension/RunAnywhereActivityExtensionLiveActivity.swift index 1527cc96a9..1cc9152334 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereActivityExtension/RunAnywhereActivityExtensionLiveActivity.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereActivityExtension/RunAnywhereActivityExtensionLiveActivity.swift @@ -4,7 +4,7 @@ // // Live Activity widget — shows the dictation flow session status in // the Dynamic Island and on the Lock Screen / StandBy. -// Branded with RunAnywhere color palette (#FF5500 primary accent). +// Branded with RunAnywhere color palette (#FF6900 primary accent). // import ActivityKit @@ -14,7 +14,7 @@ import WidgetKit // MARK: - Brand Colors (widget extension can't import main target) private enum Brand { - static let accent = Color(.sRGB, red: 1.0, green: 0.333, blue: 0.0) // #FF5500 + static let accent = Color(.sRGB, red: 1.0, green: 0.412, blue: 0.0) // #FF6900 static let accentDark = Color(.sRGB, red: 0.902, green: 0.271, blue: 0.0) // #E64500 static let green = Color(.sRGB, red: 0.063, green: 0.725, blue: 0.506) // #10B981 static let darkBg = Color(.sRGB, red: 0.059, green: 0.090, blue: 0.165) // #0F172A diff --git a/examples/ios/RunAnywhereAI/RunAnywhereKeyboard/KeyboardView.swift b/examples/ios/RunAnywhereAI/RunAnywhereKeyboard/KeyboardView.swift index cbf189460e..543ad4fa37 100644 --- a/examples/ios/RunAnywhereAI/RunAnywhereKeyboard/KeyboardView.swift +++ b/examples/ios/RunAnywhereAI/RunAnywhereKeyboard/KeyboardView.swift @@ -3,7 +3,7 @@ // RunAnywhereKeyboard // // SwiftUI keyboard UI — implements the 5-state WisprFlow-style UX. -// Branded with RunAnywhere color palette (#FF5500 primary accent). +// Branded with RunAnywhere color palette (#FF6900 primary accent). // // State machine (driven by SharedDataBridge.sessionState): // idle → full keyboard + "Run" button in toolbar @@ -20,7 +20,7 @@ import Combine // MARK: - Brand Colors (keyboard extension can't import main target) private enum Brand { - static let accent = Color(.sRGB, red: 1.0, green: 0.333, blue: 0.0) // #FF5500 + static let accent = Color(.sRGB, red: 1.0, green: 0.412, blue: 0.0) // #FF6900 static let accentDark = Color(.sRGB, red: 0.902, green: 0.271, blue: 0.0) // #E64500 static let green = Color(.sRGB, red: 0.063, green: 0.725, blue: 0.506) // #10B981 static let darkSurface = Color(white: 0.18) // key background diff --git a/examples/react-native/RunAnywhereAI/AGENTS.md b/examples/react-native/RunAnywhereAI/AGENTS.md index e41aa31cbc..8df5827cb9 100644 --- a/examples/react-native/RunAnywhereAI/AGENTS.md +++ b/examples/react-native/RunAnywhereAI/AGENTS.md @@ -128,10 +128,19 @@ Both use classic `RCT_EXTERN_MODULE` bridge pattern (not NitroModules). ### Theme System -Mirrors iOS Swift app design tokens exactly: -- `colors.ts` — 36+ named constants + dark mode overrides (dark mode not yet wired to Appearance API) -- `typography.ts` — 11 text styles matching iOS Dynamic Type sizes, Platform.select for font family -- `spacing.ts` — semantic spacing, padding, icon sizes, button heights, border radii +Single source of truth: `src/theme/system/` (a Material-3 style scheme consumed via +`useTheme()`; brand values mirror `../../DESIGN_GUIDELINE.md`). The brand primary is +RunAnywhere orange `#FF6900` in both light and dark schemes — the app previously ran a +split dual-theme setup (legacy iOS-derived blue `#007AFF` in `src/theme/{colors, +typography,spacing,index}.ts` next to an Android-derived `#E65500` scheme); the legacy +files were deleted and every consumer migrated to `useTheme()`. +- `system/colors.ts` — `brand` constants, primary tonal ramp anchored to `#FF6900`, + `lightScheme`/`darkScheme` semantic roles, `frameworkColors` badge hues +- `system/typography.ts` — Material-3 type scale (Figtree UI / MapleMono code; brand + fonts per the design guideline are a documented follow-up) +- `system/dimens.ts` / `system/motion.ts` — spacing, radii, motion tokens +- `system/themedStyles.ts` — `useThemedStyles(createStyles)` helper for color-bearing + StyleSheets, cached per light/dark scheme ## Build System Details diff --git a/examples/react-native/RunAnywhereAI/App.tsx b/examples/react-native/RunAnywhereAI/App.tsx index 8424de4e9f..94c8dd4224 100644 --- a/examples/react-native/RunAnywhereAI/App.tsx +++ b/examples/react-native/RunAnywhereAI/App.tsx @@ -26,16 +26,13 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import RootNavigator from './src/navigation/RootNavigator'; import IntroScreen from './src/features/intro/IntroScreen'; -import { ThemeProvider, useTheme } from './src/theme/system'; -import { Colors } from './src/theme/colors'; -import { Typography } from './src/theme/typography'; import { - Spacing, - Padding, - BorderRadius, - IconSize, - ButtonHeight, -} from './src/theme/spacing'; + ThemeProvider, + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from './src/theme/system'; import { RunAnywhere, SDKEnvironment } from '@runanywhere/core'; import { @@ -119,21 +116,25 @@ type InitState = 'loading' | 'ready' | 'error'; const InitializationErrorView: React.FC<{ error: string; onRetry: () => void; -}> = ({ error, onRetry }) => ( - - - - +}> = ({ error, onRetry }) => { + const { colors } = useTheme(); + const errorStyles = useThemedStyles(createErrorStyles); + return ( + + + + + + Initialization Failed + {error} + + + Retry + - Initialization Failed - {error} - - - Retry - - -); + ); +}; /** * Register backend engine plugins. Stays in App.tsx (platform/backends @@ -339,51 +340,55 @@ const styles = StyleSheet.create({ root: { flex: 1, }, - errorContainer: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - justifyContent: 'center', - alignItems: 'center', - padding: Padding.padding24, - }, - errorContent: { - alignItems: 'center', - maxWidth: 300, - }, - errorIconContainer: { - width: IconSize.huge, - height: IconSize.huge, - borderRadius: IconSize.huge / 2, - backgroundColor: Colors.badgeRed, - justifyContent: 'center', - alignItems: 'center', - marginBottom: Spacing.xLarge, - }, - errorTitle: { - ...Typography.title2, - color: Colors.textPrimary, - marginBottom: Spacing.medium, - }, - errorMessage: { - ...Typography.body, - color: Colors.textSecondary, - textAlign: 'center', - marginBottom: Spacing.xLarge, - }, - retryButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: Spacing.smallMedium, - backgroundColor: Colors.primaryBlue, - paddingHorizontal: Padding.padding24, - height: ButtonHeight.regular, - borderRadius: BorderRadius.large, - }, - retryButtonText: { - ...Typography.headline, - color: Colors.textWhite, - }, }); +const createErrorStyles = (colors: ColorScheme) => + StyleSheet.create({ + errorContainer: { + flex: 1, + backgroundColor: colors.background, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + errorContent: { + alignItems: 'center', + maxWidth: 300, + }, + errorIconContainer: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: colors.errorContainer, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }, + errorTitle: { + ...typography.titleLarge, + color: colors.onSurface, + marginBottom: 10, + }, + errorMessage: { + ...typography.bodyLarge, + color: colors.onSurfaceVariant, + textAlign: 'center', + marginBottom: 20, + }, + retryButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + backgroundColor: colors.primary, + paddingHorizontal: 24, + height: 44, + borderRadius: 12, + }, + retryButtonText: { + ...typography.titleMedium, + color: colors.onPrimary, + }, + }); + export default App; diff --git a/examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx b/examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx index 1a1f406524..74b90b3732 100644 --- a/examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx +++ b/examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx @@ -31,9 +31,12 @@ import { type LoRAState, type LoraAdapterCatalogEntry, } from '@runanywhere/proto-ts/lora_options'; -import { Colors } from '../../theme/colors'; -import { Typography } from '../../theme/typography'; -import { Spacing, Padding, BorderRadius } from '../../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../../theme/system'; interface LoRASheetProps { visible: boolean; @@ -62,6 +65,8 @@ export const LoRASheet: React.FC = ({ onClose, onAdaptersChanged, }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); const [availableAdapters, setAvailableAdapters] = useState< LoraAdapterCatalogEntry[] >([]); @@ -204,7 +209,11 @@ export const LoRASheet: React.FC = ({ {error && ( - + {error} )} @@ -233,7 +242,7 @@ export const LoRASheet: React.FC = ({ Applied @@ -242,7 +251,7 @@ export const LoRASheet: React.FC = ({ Downloaded @@ -262,7 +271,7 @@ export const LoRASheet: React.FC = ({ maximumValue={2} step={0.1} value={scale} - minimumTrackTintColor={Colors.primaryPurple} + minimumTrackTintColor={colors.primary} onValueChange={(value: number) => setScales((prev) => ({ ...prev, @@ -282,7 +291,7 @@ export const LoRASheet: React.FC = ({ {isLoadingLoRA ? ( ) : ( @@ -326,7 +335,7 @@ export const LoRASheet: React.FC = ({ @@ -336,7 +345,7 @@ export const LoRASheet: React.FC = ({ style={styles.clearAllRow} onPress={handleClearAll} > - + Clear All Adapters @@ -344,7 +353,7 @@ export const LoRASheet: React.FC = ({ {availableAdapters.length === 0 && loadedAdapters.length === 0 && ( - + No LoRA adapters available for this model. @@ -355,168 +364,148 @@ export const LoRASheet: React.FC = ({ ); }; -const styles = StyleSheet.create({ - sheetHeader: { - paddingHorizontal: Padding.padding16, - paddingTop: Spacing.small, - paddingBottom: Spacing.medium, - alignItems: 'center', - }, - container: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding12, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - title: { - ...Typography.title3, - color: Colors.textPrimary, - }, - doneButton: { - padding: Spacing.small, - }, - doneText: { - ...Typography.headline, - color: Colors.primaryBlue, - }, - content: { - padding: Padding.padding16, - }, - errorBox: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - backgroundColor: Colors.badgeRed, - borderRadius: BorderRadius.medium, - padding: Padding.padding12, - marginBottom: Spacing.medium, - }, - errorText: { - ...Typography.caption, - color: Colors.primaryRed, - flex: 1, - }, - section: { - marginBottom: Spacing.xLarge, - }, - sectionHeader: { - ...Typography.caption, - color: Colors.textSecondary, - marginBottom: Spacing.small, - }, - sectionFooter: { - ...Typography.caption2, - color: Colors.textTertiary, - marginTop: Spacing.small, - }, - card: { - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.medium, - padding: Padding.padding12, - marginBottom: Spacing.small, - }, - cardHeader: { - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'space-between', - }, - cardInfo: { - flex: 1, - marginRight: Spacing.small, - }, - adapterName: { - ...Typography.subheadline, - color: Colors.textPrimary, - }, - adapterDescription: { - ...Typography.caption, - color: Colors.textSecondary, - marginTop: 2, - }, - adapterSize: { - ...Typography.caption2, - color: Colors.textTertiary, - marginTop: 2, - }, - loadedMetaRow: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - marginTop: 2, - }, - badgeApplied: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, - badgeAppliedText: { - ...Typography.caption2, - color: Colors.primaryGreen, - }, - badgeDownloaded: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, - badgeDownloadedText: { - ...Typography.caption2, - color: Colors.primaryBlue, - }, - applyRow: { - flexDirection: 'row', - alignItems: 'flex-end', - gap: Spacing.medium, - marginTop: Spacing.small, - }, - sliderColumn: { - flex: 1, - }, - scaleLabel: { - ...Typography.caption2, - color: Colors.textSecondary, - }, - applyButton: { - backgroundColor: Colors.primaryPurple, - borderRadius: BorderRadius.medium, - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding8, - minWidth: 92, - alignItems: 'center', - }, - applyButtonDisabled: { - opacity: 0.6, - }, - applyButtonText: { - ...Typography.caption, - color: Colors.textWhite, - fontWeight: '600', - }, - clearAllRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: Spacing.small, - paddingVertical: Padding.padding12, - }, - clearAllText: { - ...Typography.subheadline, - color: Colors.primaryRed, - }, - emptyState: { - alignItems: 'center', - gap: Spacing.medium, - paddingVertical: Padding.padding40, - }, - emptyText: { - ...Typography.body, - color: Colors.textSecondary, - textAlign: 'center', - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + sheetHeader: { + paddingHorizontal: 16, + paddingTop: 6, + paddingBottom: 10, + alignItems: 'center', + }, + title: { + ...typography.titleLarge, + color: colors.onSurface, + }, + content: { + padding: 16, + }, + errorBox: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + backgroundColor: colors.errorContainer, + borderRadius: 10, + padding: 12, + marginBottom: 10, + }, + errorText: { + ...typography.bodySmall, + color: colors.onErrorContainer, + flex: 1, + }, + section: { + marginBottom: 20, + }, + sectionHeader: { + ...typography.labelMedium, + color: colors.onSurfaceVariant, + marginBottom: 6, + }, + sectionFooter: { + ...typography.labelSmall, + color: colors.outline, + marginTop: 6, + }, + card: { + backgroundColor: colors.surfaceContainer, + borderRadius: 10, + padding: 12, + marginBottom: 6, + }, + cardHeader: { + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + }, + cardInfo: { + flex: 1, + marginRight: 6, + }, + adapterName: { + ...typography.bodyMedium, + color: colors.onSurface, + }, + adapterDescription: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + marginTop: 2, + }, + adapterSize: { + ...typography.labelSmall, + color: colors.outline, + marginTop: 2, + }, + loadedMetaRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginTop: 2, + }, + badgeApplied: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + badgeAppliedText: { + ...typography.labelSmall, + color: colors.success, + }, + badgeDownloaded: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + badgeDownloadedText: { + ...typography.labelSmall, + color: colors.primary, + }, + applyRow: { + flexDirection: 'row', + alignItems: 'flex-end', + gap: 10, + marginTop: 6, + }, + sliderColumn: { + flex: 1, + }, + scaleLabel: { + ...typography.labelSmall, + color: colors.onSurfaceVariant, + }, + applyButton: { + backgroundColor: colors.primary, + borderRadius: 10, + paddingHorizontal: 16, + paddingVertical: 8, + minWidth: 92, + alignItems: 'center', + }, + applyButtonDisabled: { + opacity: 0.6, + }, + applyButtonText: { + ...typography.labelMedium, + color: colors.onPrimary, + }, + clearAllRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + paddingVertical: 12, + }, + clearAllText: { + ...typography.bodyMedium, + color: colors.error, + }, + emptyState: { + alignItems: 'center', + gap: 10, + paddingVertical: 40, + }, + emptyText: { + ...typography.bodyLarge, + color: colors.onSurfaceVariant, + textAlign: 'center', + }, + }); diff --git a/examples/react-native/RunAnywhereAI/src/components/chat/ToolCallIndicator.tsx b/examples/react-native/RunAnywhereAI/src/components/chat/ToolCallIndicator.tsx index e9ce29d4b3..4ab12cc5a8 100644 --- a/examples/react-native/RunAnywhereAI/src/components/chat/ToolCallIndicator.tsx +++ b/examples/react-native/RunAnywhereAI/src/components/chat/ToolCallIndicator.tsx @@ -16,9 +16,12 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Icon from 'react-native-vector-icons/Ionicons'; -import { Colors } from '../../theme/colors'; -import { Typography } from '../../theme/typography'; -import { Spacing, BorderRadius, Padding } from '../../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../../theme/system'; import type { ToolCallInfo } from '../../types/chat'; interface ToolCallIndicatorProps { @@ -31,19 +34,19 @@ interface ToolCallIndicatorProps { export const ToolCallIndicator: React.FC = ({ toolCallInfo, }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); const [showSheet, setShowSheet] = useState(false); const backgroundColor = toolCallInfo.success - ? Colors.primaryBlue + '1A' // 10% opacity - : Colors.primaryOrange + '1A'; + ? colors.primary + '1A' // 10% opacity + : colors.tertiary + '1A'; const borderColor = toolCallInfo.success - ? Colors.primaryBlue + '4D' // 30% opacity - : Colors.primaryOrange + '4D'; + ? colors.primary + '4D' // 30% opacity + : colors.tertiary + '4D'; - const iconColor = toolCallInfo.success - ? Colors.primaryBlue - : Colors.primaryOrange; + const iconColor = toolCallInfo.success ? colors.primary : colors.tertiary; return ( <> @@ -83,6 +86,8 @@ const ToolCallDetailSheet: React.FC = ({ toolCallInfo, onClose, }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); return ( = ({ styles.statusSection, { backgroundColor: toolCallInfo.success - ? Colors.statusGreen + '1A' - : Colors.statusRed + '1A', + ? colors.success + '1A' + : colors.error + '1A', }, ]} > {toolCallInfo.success ? 'Success' : 'Failed'} @@ -157,14 +160,17 @@ const DetailSection: React.FC = ({ title, content, isError = false, -}) => ( - - {title} - - {content} - - -); +}) => { + const styles = useThemedStyles(createStyles); + return ( + + {title} + + {content} + + + ); +}; interface CodeSectionProps { title: string; @@ -172,6 +178,7 @@ interface CodeSectionProps { } const CodeSection: React.FC = ({ title, code }) => { + const styles = useThemedStyles(createStyles); // Try to pretty print JSON let formattedCode = code; try { @@ -202,9 +209,11 @@ interface ToolCallingBadgeProps { export const ToolCallingBadge: React.FC = ({ toolCount, }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); return ( - + Tools enabled ({toolCount}) @@ -212,117 +221,116 @@ export const ToolCallingBadge: React.FC = ({ ); }; -const styles = StyleSheet.create({ - // Badge styles - badge: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingHorizontal: 10, - paddingVertical: 6, - borderRadius: 8, - borderWidth: 0.5, - marginBottom: Spacing.small, - alignSelf: 'flex-start', - }, - badgeText: { - ...Typography.caption2, - color: Colors.textSecondary, - }, +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + // Badge styles + badge: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 8, + borderWidth: 0.5, + marginBottom: 6, + alignSelf: 'flex-start', + }, + badgeText: { + ...typography.labelSmall, + color: colors.onSurfaceVariant, + }, - // Sheet styles - sheetContainer: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - }, - sheetHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding12, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - sheetTitle: { - ...Typography.headline, - color: Colors.textPrimary, - }, - closeButton: { - paddingHorizontal: Padding.padding12, - paddingVertical: Padding.padding8, - }, - closeButtonText: { - ...Typography.body, - color: Colors.primaryBlue, - fontWeight: '600', - }, - sheetContent: { - flex: 1, - }, - sheetContentContainer: { - padding: Padding.padding16, - gap: 20, - }, + // Sheet styles + sheetContainer: { + flex: 1, + backgroundColor: colors.surface, + }, + sheetHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: colors.outlineVariant, + }, + sheetTitle: { + ...typography.titleMedium, + color: colors.onSurface, + }, + closeButton: { + paddingHorizontal: 12, + paddingVertical: 8, + }, + closeButtonText: { + ...typography.bodyLarge, + color: colors.primary, + fontWeight: '600', + }, + sheetContent: { + flex: 1, + }, + sheetContentContainer: { + padding: 16, + gap: 20, + }, - // Status section - statusSection: { - flexDirection: 'row', - alignItems: 'center', - gap: 10, - padding: Padding.padding16, - borderRadius: BorderRadius.regular, - }, - statusText: { - ...Typography.headline, - color: Colors.textPrimary, - }, + // Status section + statusSection: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + padding: 16, + borderRadius: 8, + }, + statusText: { + ...typography.titleMedium, + color: colors.onSurface, + }, - // Detail section - section: { - gap: Spacing.small, - }, - sectionTitle: { - ...Typography.caption, - color: Colors.textSecondary, - }, - sectionContent: { - ...Typography.body, - color: Colors.textPrimary, - }, - errorText: { - color: Colors.statusRed, - }, + // Detail section + section: { + gap: 6, + }, + sectionTitle: { + ...typography.labelMedium, + color: colors.onSurfaceVariant, + }, + sectionContent: { + ...typography.bodyLarge, + color: colors.onSurface, + }, + errorText: { + color: colors.error, + }, - // Code section - codeContainer: { - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.regular, - padding: Padding.padding12, - }, - codeText: { - ...Typography.footnote, - fontFamily: 'Menlo', - color: Colors.textPrimary, - }, + // Code section + codeContainer: { + backgroundColor: colors.surfaceContainer, + borderRadius: 8, + padding: 12, + }, + codeText: { + ...typography.codeSmall, + color: colors.onSurface, + }, - // Tool calling badge (above input) - toolCallingBadge: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 6, - paddingHorizontal: Padding.padding12, - paddingVertical: Padding.padding8, - backgroundColor: Colors.primaryBlue + '1A', - borderTopWidth: 1, - borderTopColor: Colors.borderLight, - }, - toolCallingBadgeText: { - ...Typography.caption, - color: Colors.primaryBlue, - fontWeight: '500', - }, -}); + // Tool calling badge (above input) + toolCallingBadge: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: colors.primary + '1A', + borderTopWidth: 1, + borderTopColor: colors.outlineVariant, + }, + toolCallingBadgeText: { + ...typography.labelMedium, + color: colors.primary, + }, + }); export default ToolCallIndicator; diff --git a/examples/react-native/RunAnywhereAI/src/components/chat/TypingIndicator.tsx b/examples/react-native/RunAnywhereAI/src/components/chat/TypingIndicator.tsx index 4193345652..479c023451 100644 --- a/examples/react-native/RunAnywhereAI/src/components/chat/TypingIndicator.tsx +++ b/examples/react-native/RunAnywhereAI/src/components/chat/TypingIndicator.tsx @@ -8,9 +8,11 @@ import React, { useEffect, useRef } from 'react'; import { View, Text, StyleSheet, Animated } from 'react-native'; -import { Colors } from '../../theme/colors'; -import { Typography } from '../../theme/typography'; -import { Spacing, BorderRadius, Padding } from '../../theme/spacing'; +import { + typography, + useThemedStyles, + type ColorScheme, +} from '../../theme/system'; interface TypingIndicatorProps { /** Label text */ @@ -20,6 +22,7 @@ interface TypingIndicatorProps { export const TypingIndicator: React.FC = ({ label = 'AI is thinking...', }) => { + const styles = useThemedStyles(createStyles); // Animation values for each dot const dot1 = useRef(new Animated.Value(0)).current; const dot2 = useRef(new Animated.Value(0)).current; @@ -87,36 +90,37 @@ export const TypingIndicator: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - alignItems: 'flex-start', - paddingHorizontal: Padding.padding16, - marginVertical: Spacing.xSmall, - }, - bubble: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.smallMedium, - backgroundColor: Colors.backgroundGray5, - borderRadius: BorderRadius.xLarge, - borderBottomLeftRadius: BorderRadius.small, - paddingHorizontal: Padding.padding14, - paddingVertical: Padding.padding10, - }, - dotsContainer: { - flexDirection: 'row', - gap: Spacing.xSmall, - }, - dot: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: Colors.textSecondary, - }, - label: { - ...Typography.footnote, - color: Colors.textSecondary, - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + alignItems: 'flex-start', + paddingHorizontal: 16, + marginVertical: 4, + }, + bubble: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: colors.surfaceContainerHighest, + borderRadius: 16, + borderBottomLeftRadius: 4, + paddingHorizontal: 14, + paddingVertical: 10, + }, + dotsContainer: { + flexDirection: 'row', + gap: 4, + }, + dot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.onSurfaceVariant, + }, + label: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + }, + }); export default TypingIndicator; diff --git a/examples/react-native/RunAnywhereAI/src/components/common/LoadingOverlay.tsx b/examples/react-native/RunAnywhereAI/src/components/common/LoadingOverlay.tsx index ee2d2e15cc..df2ab9f8de 100644 --- a/examples/react-native/RunAnywhereAI/src/components/common/LoadingOverlay.tsx +++ b/examples/react-native/RunAnywhereAI/src/components/common/LoadingOverlay.tsx @@ -8,9 +8,12 @@ import React from 'react'; import { View, Text, StyleSheet, ActivityIndicator, Modal } from 'react-native'; -import { Colors } from '../../theme/colors'; -import { Typography } from '../../theme/typography'; -import { Spacing, BorderRadius, Padding } from '../../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../../theme/system'; interface LoadingOverlayProps { /** Whether to show the overlay */ @@ -29,6 +32,9 @@ export const LoadingOverlay: React.FC = ({ progress, modal = true, }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); + if (!visible) { return null; } @@ -36,7 +42,7 @@ export const LoadingOverlay: React.FC = ({ const content = ( - + {message && {message}} @@ -67,53 +73,54 @@ export const LoadingOverlay: React.FC = ({ return content; }; -const styles = StyleSheet.create({ - container: { - ...StyleSheet.absoluteFill, - backgroundColor: Colors.overlayLight, - justifyContent: 'center', - alignItems: 'center', - }, - card: { - backgroundColor: Colors.backgroundPrimary, - borderRadius: BorderRadius.xLarge, - padding: Padding.padding30, - alignItems: 'center', - minWidth: 200, - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.15, - shadowRadius: 12, - elevation: 8, - }, - message: { - ...Typography.body, - color: Colors.textPrimary, - marginTop: Spacing.large, - textAlign: 'center', - }, - progressContainer: { - width: '100%', - marginTop: Spacing.large, - alignItems: 'center', - }, - progressBar: { - width: '100%', - height: 6, - backgroundColor: Colors.backgroundGray5, - borderRadius: 3, - overflow: 'hidden', - }, - progressFill: { - height: '100%', - backgroundColor: Colors.primaryBlue, - borderRadius: 3, - }, - progressText: { - ...Typography.caption, - color: Colors.textSecondary, - marginTop: Spacing.small, - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + ...StyleSheet.absoluteFill, + backgroundColor: 'rgba(0, 0, 0, 0.3)', + justifyContent: 'center', + alignItems: 'center', + }, + card: { + backgroundColor: colors.surface, + borderRadius: 16, + padding: 30, + alignItems: 'center', + minWidth: 200, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 8, + }, + message: { + ...typography.bodyLarge, + color: colors.onSurface, + marginTop: 16, + textAlign: 'center', + }, + progressContainer: { + width: '100%', + marginTop: 16, + alignItems: 'center', + }, + progressBar: { + width: '100%', + height: 6, + backgroundColor: colors.surfaceContainerHighest, + borderRadius: 3, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: colors.primary, + borderRadius: 3, + }, + progressText: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + marginTop: 6, + }, + }); export default LoadingOverlay; diff --git a/examples/react-native/RunAnywhereAI/src/components/common/ModelStatusBanner.tsx b/examples/react-native/RunAnywhereAI/src/components/common/ModelStatusBanner.tsx index 0fccc71f1e..7117527736 100644 --- a/examples/react-native/RunAnywhereAI/src/components/common/ModelStatusBanner.tsx +++ b/examples/react-native/RunAnywhereAI/src/components/common/ModelStatusBanner.tsx @@ -15,15 +15,15 @@ import { ActivityIndicator, } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; -import { Colors } from '../../theme/colors'; -import { Typography } from '../../theme/typography'; -import { Spacing, BorderRadius, Padding } from '../../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../../theme/system'; import type { InferenceFramework } from '@runanywhere/proto-ts/model_types'; import { RunAnywhere } from '@runanywhere/core'; -import { - getFrameworkColor, - getFrameworkIcon, -} from '../../utils/modelDisplay'; +import { getFrameworkColor, getFrameworkIcon } from '../../utils/modelDisplay'; interface ModelStatusBannerProps { /** Model name if loaded */ @@ -48,12 +48,15 @@ export const ModelStatusBanner: React.FC = ({ onSelectModel, placeholder = 'Select a model to get started', }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); + // Loading state if (isLoading) { return ( - + Loading model... {loadProgress !== undefined && @@ -80,16 +83,12 @@ export const ModelStatusBanner: React.FC = ({ activeOpacity={0.7} > - + {placeholder} Select Model - + ); @@ -134,98 +133,99 @@ export const ModelStatusBanner: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.medium, - padding: Padding.padding12, - marginHorizontal: Padding.padding16, - marginVertical: Spacing.small, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - emptyContainer: { - borderWidth: 1, - borderColor: Colors.borderLight, - borderStyle: 'dashed', - }, - emptyContent: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.smallMedium, - }, - emptyText: { - ...Typography.subheadline, - color: Colors.textSecondary, - }, - selectButton: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.xSmall, - }, - selectButtonText: { - ...Typography.subheadline, - color: Colors.primaryBlue, - fontWeight: '600', - }, - loadingContent: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.smallMedium, - flex: 1, - }, - loadingText: { - ...Typography.subheadline, - color: Colors.textSecondary, - }, - progressBar: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - height: 3, - backgroundColor: Colors.backgroundGray5, - borderBottomLeftRadius: BorderRadius.medium, - borderBottomRightRadius: BorderRadius.medium, - overflow: 'hidden', - }, - progressFill: { - height: '100%', - backgroundColor: Colors.primaryBlue, - }, - loadedContent: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.smallMedium, - flex: 1, - }, - frameworkBadge: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.xSmall, - paddingHorizontal: Spacing.smallMedium, - paddingVertical: Spacing.xSmall, - borderRadius: BorderRadius.small, - }, - frameworkText: { - ...Typography.caption, - fontWeight: '600', - }, - modelName: { - ...Typography.subheadline, - color: Colors.textPrimary, - flex: 1, - }, - changeButton: { - paddingHorizontal: Spacing.medium, - paddingVertical: Spacing.small, - }, - changeButtonText: { - ...Typography.subheadline, - color: Colors.primaryBlue, - fontWeight: '600', - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + backgroundColor: colors.surfaceContainer, + borderRadius: 10, + padding: 12, + marginHorizontal: 16, + marginVertical: 6, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + emptyContainer: { + borderWidth: 1, + borderColor: colors.outlineVariant, + borderStyle: 'dashed', + }, + emptyContent: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + emptyText: { + ...typography.bodyMedium, + color: colors.onSurfaceVariant, + }, + selectButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + selectButtonText: { + ...typography.bodyMedium, + color: colors.primary, + fontWeight: '600', + }, + loadingContent: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + flex: 1, + }, + loadingText: { + ...typography.bodyMedium, + color: colors.onSurfaceVariant, + }, + progressBar: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 3, + backgroundColor: colors.surfaceContainerHighest, + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: colors.primary, + }, + loadedContent: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + flex: 1, + }, + frameworkBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + }, + frameworkText: { + ...typography.bodySmall, + fontWeight: '600', + }, + modelName: { + ...typography.bodyMedium, + color: colors.onSurface, + flex: 1, + }, + changeButton: { + paddingHorizontal: 10, + paddingVertical: 6, + }, + changeButtonText: { + ...typography.bodyMedium, + color: colors.primary, + fontWeight: '600', + }, + }); export default ModelStatusBanner; diff --git a/examples/react-native/RunAnywhereAI/src/navigation/BottomTabs.tsx b/examples/react-native/RunAnywhereAI/src/navigation/BottomTabs.tsx index 33da341726..5a90745fdd 100644 --- a/examples/react-native/RunAnywhereAI/src/navigation/BottomTabs.tsx +++ b/examples/react-native/RunAnywhereAI/src/navigation/BottomTabs.tsx @@ -4,8 +4,7 @@ */ import React from 'react'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { Typography } from '../theme/typography'; -import { Icon, type IconName, useTheme } from '../theme/system'; +import { Icon, type IconName, typography, useTheme } from '../theme/system'; import type { TabParamList } from './navigation.types'; import { ROUTES } from './routes'; import ChatScreen from '../screens/ChatScreen'; @@ -33,27 +32,27 @@ export const BottomTabs: React.FC = () => { backgroundColor: colors.surface, borderTopColor: colors.outlineVariant, }, - tabBarLabelStyle: { ...Typography.caption2 }, + tabBarLabelStyle: { ...typography.labelSmall }, tabBarIcon: ({ color, size }) => ( ), })} > - - - + + + ); }; diff --git a/examples/react-native/RunAnywhereAI/src/screens/ChatAnalyticsScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/ChatAnalyticsScreen.tsx index 9100b108c2..44ee238642 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/ChatAnalyticsScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/ChatAnalyticsScreen.tsx @@ -20,9 +20,12 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Icon from 'react-native-vector-icons/Ionicons'; -import { Colors } from '../theme/colors'; -import { Typography } from '../theme/typography'; -import { Spacing, Padding, BorderRadius } from '../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../theme/system'; import type { Message, MessageAnalytics, Conversation } from '../types/chat'; import { MessageRole } from '../types/chat'; @@ -52,15 +55,18 @@ const PerformanceCard: React.FC = ({ value, icon, color, -}) => ( - - - +}) => { + const styles = useThemedStyles(createStyles); + return ( + + + + + {value} + {title} - {value} - {title} - -); + ); +}; /** * Metric View Component @@ -71,12 +77,15 @@ interface MetricViewProps { color: string; } -const MetricView: React.FC = ({ label, value, color }) => ( - - {value} - {label} - -); +const MetricView: React.FC = ({ label, value, color }) => { + const styles = useThemedStyles(createStyles); + return ( + + {value} + {label} + + ); +}; /** * Message Analytics Row Component @@ -91,64 +100,70 @@ const MessageAnalyticsRow: React.FC = ({ messageNumber, message, analytics, -}) => ( - - - Message #{messageNumber} - - {message.modelInfo && ( - - - {message.modelInfo.modelName} - - - )} - {message.modelInfo?.framework && ( - - - {message.modelInfo.framework} - - - )} +}) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); + return ( + + + Message #{messageNumber} + + {message.modelInfo && ( + + + {message.modelInfo.modelName} + + + )} + {message.modelInfo?.framework && ( + + + {message.modelInfo.framework} + + + )} + - - - - {analytics.timeToFirstToken && ( + - )} - {analytics.performance.throughputTokensPerSec > 0 && ( - - )} - {analytics.wasThinkingMode && ( - - )} - + {analytics.timeToFirstToken && ( + + )} + {analytics.performance.throughputTokensPerSec > 0 && ( + + )} + {analytics.wasThinkingMode && ( + + )} + - - {message.content.slice(0, 100)} - - -); + + {message.content.slice(0, 100)} + + + ); +}; export const ChatAnalyticsScreen: React.FC = ({ messages, conversation, onClose, }) => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); const [activeTab, setActiveTab] = useState('overview'); // Extract analytics from messages @@ -266,7 +281,7 @@ export const ChatAnalyticsScreen: React.FC = ({ name="stats-chart" size={18} color={ - activeTab === 'overview' ? Colors.primaryBlue : Colors.textSecondary + activeTab === 'overview' ? colors.primary : colors.onSurfaceVariant } /> = ({ name="chatbubbles-outline" size={18} color={ - activeTab === 'messages' ? Colors.primaryBlue : Colors.textSecondary + activeTab === 'messages' ? colors.primary : colors.onSurfaceVariant } /> = ({ size={18} color={ activeTab === 'performance' - ? Colors.primaryBlue - : Colors.textSecondary + ? colors.primary + : colors.onSurfaceVariant } /> = ({ {conversationSummary} {conversation && ( - + Created {new Date(conversation.createdAt).toLocaleDateString()} @@ -351,7 +366,7 @@ export const ChatAnalyticsScreen: React.FC = ({ )} {analyticsMessages.length > 0 && ( - + {metrics.modelsUsed.size} model {metrics.modelsUsed.size === 1 ? '' : 's'} used @@ -369,25 +384,25 @@ export const ChatAnalyticsScreen: React.FC = ({ title="Avg Response Time" value={`${metrics.averageResponseTime.toFixed(1)}s`} icon="timer-outline" - color={Colors.statusGreen} + color={colors.success} /> @@ -395,11 +410,7 @@ export const ChatAnalyticsScreen: React.FC = ({ {analyticsMessages.length === 0 && ( - + No analytics data available yet Start a conversation to see performance metrics @@ -427,11 +438,7 @@ export const ChatAnalyticsScreen: React.FC = ({ showsVerticalScrollIndicator={false} ListEmptyComponent={ - + No messages with analytics } @@ -473,7 +480,7 @@ export const ChatAnalyticsScreen: React.FC = ({ Thinking Mode Analysis - + Used in {metrics.thinkingModeCount} messages ( {Math.round(metrics.thinkingModePercentage)}%) @@ -484,11 +491,7 @@ export const ChatAnalyticsScreen: React.FC = ({ {analyticsMessages.length === 0 && ( - + No performance data available )} @@ -530,236 +533,237 @@ export const ChatAnalyticsScreen: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: Colors.backgroundGrouped, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding12, - backgroundColor: Colors.backgroundPrimary, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - title: { - ...Typography.headline, - color: Colors.textPrimary, - }, - closeButton: { - paddingVertical: Spacing.small, - paddingHorizontal: Spacing.medium, - }, - closeButtonText: { - ...Typography.body, - color: Colors.primaryBlue, - fontWeight: '600', - }, - tabsContainer: { - flexDirection: 'row', - backgroundColor: Colors.backgroundPrimary, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - tab: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: Padding.padding12, - gap: Spacing.xSmall, - }, - tabActive: { - borderBottomWidth: 2, - borderBottomColor: Colors.primaryBlue, - }, - tabText: { - ...Typography.footnote, - color: Colors.textSecondary, - }, - tabTextActive: { - color: Colors.primaryBlue, - fontWeight: '600', - }, - tabContent: { - flex: 1, - padding: Padding.padding16, - }, - card: { - backgroundColor: Colors.backgroundPrimary, - borderRadius: BorderRadius.medium, - padding: Padding.padding16, - marginBottom: Spacing.medium, - }, - cardTitle: { - ...Typography.headline, - color: Colors.textPrimary, - marginBottom: Spacing.medium, - }, - summaryRow: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.smallMedium, - marginBottom: Spacing.small, - }, - summaryText: { - ...Typography.subheadline, - color: Colors.textPrimary, - }, - performanceGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: Spacing.medium, - }, - performanceCard: { - flex: 1, - minWidth: '45%', - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.regular, - padding: Padding.padding12, - borderWidth: 1, - }, - performanceCardHeader: { - marginBottom: Spacing.small, - }, - performanceCardValue: { - ...Typography.title2, - color: Colors.textPrimary, - marginBottom: Spacing.xxSmall, - }, - performanceCardTitle: { - ...Typography.caption, - color: Colors.textSecondary, - }, - metricView: { - alignItems: 'center', - }, - metricValue: { - ...Typography.footnote, - fontWeight: '600', - }, - metricLabel: { - ...Typography.caption2, - color: Colors.textSecondary, - }, - messagesList: { - padding: Padding.padding16, - }, - messageRow: { - backgroundColor: Colors.backgroundPrimary, - borderRadius: BorderRadius.regular, - padding: Padding.padding16, - marginBottom: Spacing.medium, - }, - messageRowHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: Spacing.small, - }, - messageRowTitle: { - ...Typography.subheadline, - color: Colors.textPrimary, - fontWeight: '600', - }, - messageRowBadges: { - flexDirection: 'row', - gap: Spacing.small, - }, - badge: { - paddingHorizontal: Spacing.small, - paddingVertical: Spacing.xxSmall, - borderRadius: BorderRadius.small, - }, - badgeBlue: { - backgroundColor: Colors.badgeBlue, - }, - badgePurple: { - backgroundColor: Colors.badgePurple, - }, - badgeTextBlue: { - ...Typography.caption2, - color: Colors.primaryBlue, - fontWeight: '600', - }, - badgeTextPurple: { - ...Typography.caption2, - color: Colors.primaryPurple, - fontWeight: '600', - }, - metricsRow: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.large, - marginBottom: Spacing.small, - }, - messagePreview: { - ...Typography.caption, - color: Colors.textSecondary, - }, - modelRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - backgroundColor: Colors.backgroundSecondary, - borderRadius: BorderRadius.regular, - padding: Padding.padding12, - marginBottom: Spacing.small, - }, - modelInfo: { - flex: 1, - }, - modelName: { - ...Typography.subheadline, - color: Colors.textPrimary, - fontWeight: '500', - }, - modelMessages: { - ...Typography.caption, - color: Colors.textSecondary, - }, - modelStats: { - alignItems: 'flex-end', - }, - modelStatValue: { - ...Typography.caption, - color: Colors.statusGreen, - }, - modelStatSpeed: { - ...Typography.caption, - color: Colors.primaryBlue, - }, - thinkingAnalysis: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.smallMedium, - backgroundColor: `${Colors.primaryPurple}10`, - borderRadius: BorderRadius.regular, - padding: Padding.padding12, - }, - thinkingText: { - ...Typography.subheadline, - color: Colors.textPrimary, - }, - emptyState: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: Padding.padding40, - }, - emptyText: { - ...Typography.body, - color: Colors.textSecondary, - marginTop: Spacing.medium, - }, - emptySubtext: { - ...Typography.footnote, - color: Colors.textTertiary, - marginTop: Spacing.xSmall, - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.surfaceContainer, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: colors.surface, + borderBottomWidth: 1, + borderBottomColor: colors.outlineVariant, + }, + title: { + ...typography.titleMedium, + color: colors.onSurface, + }, + closeButton: { + paddingVertical: 6, + paddingHorizontal: 10, + }, + closeButtonText: { + ...typography.bodyLarge, + color: colors.primary, + fontWeight: '600', + }, + tabsContainer: { + flexDirection: 'row', + backgroundColor: colors.surface, + borderBottomWidth: 1, + borderBottomColor: colors.outlineVariant, + }, + tab: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 12, + gap: 4, + }, + tabActive: { + borderBottomWidth: 2, + borderBottomColor: colors.primary, + }, + tabText: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + }, + tabTextActive: { + color: colors.primary, + fontWeight: '600', + }, + tabContent: { + flex: 1, + padding: 16, + }, + card: { + backgroundColor: colors.surface, + borderRadius: 10, + padding: 16, + marginBottom: 10, + }, + cardTitle: { + ...typography.titleMedium, + color: colors.onSurface, + marginBottom: 10, + }, + summaryRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 6, + }, + summaryText: { + ...typography.bodyMedium, + color: colors.onSurface, + }, + performanceGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + performanceCard: { + flex: 1, + minWidth: '45%', + backgroundColor: colors.surfaceContainer, + borderRadius: 8, + padding: 12, + borderWidth: 1, + }, + performanceCardHeader: { + marginBottom: 6, + }, + performanceCardValue: { + ...typography.titleLarge, + color: colors.onSurface, + marginBottom: 2, + }, + performanceCardTitle: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + }, + metricView: { + alignItems: 'center', + }, + metricValue: { + ...typography.bodySmall, + fontWeight: '600', + }, + metricLabel: { + ...typography.labelSmall, + color: colors.onSurfaceVariant, + }, + messagesList: { + padding: 16, + }, + messageRow: { + backgroundColor: colors.surface, + borderRadius: 8, + padding: 16, + marginBottom: 10, + }, + messageRowHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 6, + }, + messageRowTitle: { + ...typography.bodyMedium, + color: colors.onSurface, + fontWeight: '600', + }, + messageRowBadges: { + flexDirection: 'row', + gap: 6, + }, + badge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + badgeModel: { + backgroundColor: colors.primaryContainer, + }, + badgeFramework: { + backgroundColor: colors.tertiaryContainer, + }, + badgeTextModel: { + ...typography.labelSmall, + color: colors.onPrimaryContainer, + fontWeight: '600', + }, + badgeTextFramework: { + ...typography.labelSmall, + color: colors.onTertiaryContainer, + fontWeight: '600', + }, + metricsRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 16, + marginBottom: 6, + }, + messagePreview: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + }, + modelRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: colors.surfaceContainer, + borderRadius: 8, + padding: 12, + marginBottom: 6, + }, + modelInfo: { + flex: 1, + }, + modelName: { + ...typography.bodyMedium, + color: colors.onSurface, + fontWeight: '500', + }, + modelMessages: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + }, + modelStats: { + alignItems: 'flex-end', + }, + modelStatValue: { + ...typography.bodySmall, + color: colors.success, + }, + modelStatSpeed: { + ...typography.bodySmall, + color: colors.primary, + }, + thinkingAnalysis: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: `${colors.tertiary}10`, + borderRadius: 8, + padding: 12, + }, + thinkingText: { + ...typography.bodyMedium, + color: colors.onSurface, + }, + emptyState: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40, + }, + emptyText: { + ...typography.bodyLarge, + color: colors.onSurfaceVariant, + marginTop: 10, + }, + emptySubtext: { + ...typography.bodySmall, + color: colors.outline, + marginTop: 4, + }, + }); export default ChatAnalyticsScreen; diff --git a/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx index 6e2092a7d2..99767c8a37 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx @@ -37,9 +37,12 @@ import { SafeAreaView, useSafeAreaInsets, } from 'react-native-safe-area-context'; -import { Colors } from '../theme/colors'; -import { Typography } from '../theme/typography'; -import { Spacing, Padding, IconSize } from '../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../theme/system'; import { ModelRequiredOverlay } from '../components/common'; import { ChatHeader } from '../features/chat/components/ChatHeader'; import { PromptSuggestions } from '../features/chat/components/PromptSuggestions'; @@ -112,6 +115,8 @@ function makeToolCallInfo( } export const ChatScreen: React.FC = () => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); // Conversation store const { conversations, @@ -703,8 +708,8 @@ export const ChatScreen: React.FC = () => { Start a conversation @@ -784,9 +789,7 @@ export const ChatScreen: React.FC = () => { name="sparkles" size={14} color={ - loraAdapterCount > 0 - ? Colors.textWhite - : Colors.primaryPurple + loraAdapterCount > 0 ? colors.onPrimary : colors.primary } /> { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: Padding.padding16, - paddingTop: 0, - paddingBottom: Padding.padding12, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - titleContainer: { - alignItems: 'center', - }, - title: { - ...Typography.title2, - color: Colors.textPrimary, - }, - conversationCount: { - ...Typography.caption2, - color: Colors.textTertiary, - marginTop: 2, - }, - headerActions: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - }, - headerButton: { - padding: Spacing.small, - }, - headerButtonDisabled: { - opacity: 0.5, - }, - list: { - flex: 1, - }, - messagesList: { - paddingVertical: Spacing.medium, - }, - emptyList: { - flexGrow: 1, - justifyContent: 'center', - }, - emptyState: { - alignItems: 'center', - padding: Padding.padding40, - }, - emptyIconContainer: { - width: IconSize.huge, - height: IconSize.huge, - borderRadius: IconSize.huge / 2, - backgroundColor: Colors.backgroundSecondary, - justifyContent: 'center', - alignItems: 'center', - marginBottom: Spacing.large, - }, - emptyTitle: { - ...Typography.title3, - color: Colors.textPrimary, - marginBottom: Spacing.small, - }, - emptySubtitle: { - ...Typography.body, - color: Colors.textSecondary, - textAlign: 'center', - maxWidth: 280, - }, - loraRow: { - flexDirection: 'row', - paddingHorizontal: Padding.padding16, - paddingTop: 2, - paddingBottom: 6, - }, - loraPill: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - borderWidth: 1, - borderColor: Colors.primaryPurple, - borderRadius: 14, - paddingHorizontal: Padding.padding12, - paddingVertical: 4, - }, - loraPillActive: { - backgroundColor: Colors.primaryPurple, - }, - loraPillText: { - ...Typography.caption2, - color: Colors.primaryPurple, - }, - loraPillTextActive: { - color: Colors.textWhite, - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + list: { + flex: 1, + }, + messagesList: { + paddingVertical: 10, + }, + emptyList: { + flexGrow: 1, + justifyContent: 'center', + }, + emptyState: { + alignItems: 'center', + padding: 40, + }, + emptyIconContainer: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: colors.surfaceContainer, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + }, + emptyTitle: { + ...typography.titleLarge, + color: colors.onSurface, + marginBottom: 6, + }, + emptySubtitle: { + ...typography.bodyLarge, + color: colors.onSurfaceVariant, + textAlign: 'center', + maxWidth: 280, + }, + loraRow: { + flexDirection: 'row', + paddingHorizontal: 16, + paddingTop: 2, + paddingBottom: 6, + }, + loraPill: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + borderWidth: 1, + borderColor: colors.primary, + borderRadius: 14, + paddingHorizontal: 12, + paddingVertical: 4, + }, + loraPillActive: { + backgroundColor: colors.primary, + }, + loraPillText: { + ...typography.labelSmall, + color: colors.primary, + }, + loraPillTextActive: { + color: colors.onPrimary, + }, + }); export default ChatScreen; diff --git a/examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx index b4c9e0c948..0a4394ef87 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx @@ -12,9 +12,12 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { RunAnywhere } from '@runanywhere/core'; import type { StorageInfo } from '@runanywhere/proto-ts/storage_types'; import type { ModelInfo } from '@runanywhere/proto-ts/model_types'; -import { Colors } from '../theme/colors'; -import { Typography } from '../theme/typography'; -import { Spacing, Padding, BorderRadius } from '../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../theme/system'; import { DEFAULT_INFERENCE_FRAMEWORK, getFrameworkColor, @@ -37,6 +40,8 @@ function formatBytes(bytes: number): string { } export const StorageScreen: React.FC = () => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); const [storageInfo, setStorageInfo] = useState(null); const [downloadedModels, setDownloadedModels] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); @@ -96,7 +101,7 @@ export const StorageScreen: React.FC = () => { @@ -124,18 +129,14 @@ export const StorageScreen: React.FC = () => { - + Clear Cache - + Clean Temp @@ -146,12 +147,17 @@ export const StorageScreen: React.FC = () => { No downloaded models. ) : ( downloadedModels.map((model) => { - const framework = getPrimaryFramework(model, DEFAULT_INFERENCE_FRAMEWORK); + const framework = getPrimaryFramework( + model, + DEFAULT_INFERENCE_FRAMEWORK + ); const frameworkColor = getFrameworkColor(framework); return ( - {model.name || model.id} + + {model.name || model.id} + {formatBytes(getModelDownloadSizeBytes(model))} @@ -167,7 +173,12 @@ export const StorageScreen: React.FC = () => { size={12} color={frameworkColor} /> - + {RunAnywhere.formatFramework(framework)} @@ -177,11 +188,7 @@ export const StorageScreen: React.FC = () => { style={styles.deleteButton} onPress={() => deleteModel(model)} > - + ); @@ -196,130 +203,134 @@ export const StorageScreen: React.FC = () => { const StorageRow: React.FC<{ label: string; value: string }> = ({ label, value, -}) => ( - - {label} - {value} - -); +}) => { + const styles = useThemedStyles(createStyles); + return ( + + {label} + {value} + + ); +}; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding12, - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - title: { - ...Typography.title2, - color: Colors.textPrimary, - }, - content: { - flex: 1, - }, - contentInner: { - padding: Padding.padding16, - gap: Spacing.large, - }, - sectionTitle: { - ...Typography.headline, - color: Colors.textPrimary, - }, - section: { - borderRadius: BorderRadius.regular, - backgroundColor: Colors.backgroundSecondary, - overflow: 'hidden', - }, - storageRow: { - minHeight: 48, - paddingHorizontal: Padding.padding16, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - storageLabel: { - ...Typography.body, - color: Colors.textSecondary, - }, - storageValue: { - ...Typography.body, - color: Colors.textPrimary, - fontWeight: '600', - }, - actionRow: { - flexDirection: 'row', - gap: Spacing.medium, - }, - actionButton: { - flex: 1, - minHeight: 44, - borderRadius: BorderRadius.regular, - backgroundColor: Colors.backgroundSecondary, - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - gap: Spacing.small, - }, - actionButtonText: { - ...Typography.subheadline, - color: Colors.textPrimary, - }, - emptyText: { - ...Typography.body, - color: Colors.textSecondary, - padding: Padding.padding16, - }, - modelRow: { - minHeight: 64, - paddingHorizontal: Padding.padding16, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderBottomWidth: 1, - borderBottomColor: Colors.borderLight, - }, - modelText: { - flex: 1, - }, - modelMeta: { - flexDirection: 'row', - alignItems: 'center', - gap: Spacing.small, - marginTop: 4, - }, - modelName: { - ...Typography.subheadline, - color: Colors.textPrimary, - fontWeight: '600', - }, - modelSize: { - ...Typography.footnote, - color: Colors.textSecondary, - }, - backendBadge: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - borderRadius: 999, - paddingHorizontal: 8, - paddingVertical: 3, - }, - backendText: { - ...Typography.caption2, - fontWeight: '700', - }, - deleteButton: { - padding: Padding.padding10, - }, -}); +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: colors.outlineVariant, + }, + title: { + ...typography.titleLarge, + color: colors.onSurface, + }, + content: { + flex: 1, + }, + contentInner: { + padding: 16, + gap: 16, + }, + sectionTitle: { + ...typography.titleMedium, + color: colors.onSurface, + }, + section: { + borderRadius: 8, + backgroundColor: colors.surfaceContainer, + overflow: 'hidden', + }, + storageRow: { + minHeight: 48, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderBottomWidth: 1, + borderBottomColor: colors.outlineVariant, + }, + storageLabel: { + ...typography.bodyLarge, + color: colors.onSurfaceVariant, + }, + storageValue: { + ...typography.bodyLarge, + color: colors.onSurface, + fontWeight: '600', + }, + actionRow: { + flexDirection: 'row', + gap: 10, + }, + actionButton: { + flex: 1, + minHeight: 44, + borderRadius: 8, + backgroundColor: colors.surfaceContainer, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + gap: 6, + }, + actionButtonText: { + ...typography.bodyMedium, + color: colors.onSurface, + }, + emptyText: { + ...typography.bodyLarge, + color: colors.onSurfaceVariant, + padding: 16, + }, + modelRow: { + minHeight: 64, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderBottomWidth: 1, + borderBottomColor: colors.outlineVariant, + }, + modelText: { + flex: 1, + }, + modelMeta: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginTop: 4, + }, + modelName: { + ...typography.bodyMedium, + color: colors.onSurface, + fontWeight: '600', + }, + modelSize: { + ...typography.bodySmall, + color: colors.onSurfaceVariant, + }, + backendBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + borderRadius: 999, + paddingHorizontal: 8, + paddingVertical: 3, + }, + backendText: { + ...typography.labelSmall, + fontWeight: '700', + }, + deleteButton: { + padding: 10, + }, + }); export default StorageScreen; diff --git a/examples/react-native/RunAnywhereAI/src/screens/VLMScreen.tsx b/examples/react-native/RunAnywhereAI/src/screens/VLMScreen.tsx index 44089307dc..bd8024df6a 100644 --- a/examples/react-native/RunAnywhereAI/src/screens/VLMScreen.tsx +++ b/examples/react-native/RunAnywhereAI/src/screens/VLMScreen.tsx @@ -33,11 +33,16 @@ import { ModelSelectionContext, } from '../components/model/ModelSelectionSheet'; import { type ModelInfo as SDKModelInfo } from '@runanywhere/proto-ts/model_types'; -import { Colors } from '../theme/colors'; -import { Typography } from '../theme/typography'; -import { Spacing, Padding, BorderRadius } from '../theme/spacing'; +import { + typography, + useTheme, + useThemedStyles, + type ColorScheme, +} from '../theme/system'; const VLMScreen: React.FC = () => { + const { colors } = useTheme(); + const styles = useThemedStyles(createStyles); const { height: screenHeight } = useWindowDimensions(); const cameraRef = useRef(null); const device = useCameraDevice('back'); @@ -96,12 +101,12 @@ const VLMScreen: React.FC = () => { vlm.clearError(); }, [vlm]); - // Main action button color + // Main action button color — brand primary for capture, error red for stop const mainButtonColor = vlm.isAutoStreaming - ? Colors.primaryRed + ? colors.error : vlm.isProcessing - ? Colors.textTertiary - : Colors.primaryOrange; + ? colors.outline + : colors.primary; return ( @@ -111,7 +116,7 @@ const VLMScreen: React.FC = () => { Vision AI @@ -146,7 +151,7 @@ const VLMScreen: React.FC = () => { @@ -174,7 +179,7 @@ const VLMScreen: React.FC = () => { {vlm.isProcessing && ( - + Analyzing... @@ -202,7 +207,7 @@ const VLMScreen: React.FC = () => { )} @@ -220,7 +225,7 @@ const VLMScreen: React.FC = () => { @@ -256,9 +261,7 @@ const VLMScreen: React.FC = () => { @@ -275,7 +278,7 @@ const VLMScreen: React.FC = () => { @@ -291,10 +294,10 @@ const VLMScreen: React.FC = () => { size={24} color={ vlm.isAutoStreaming - ? Colors.statusGreen + ? colors.success : vlm.isProcessing - ? Colors.textTertiary - : Colors.primaryBlue + ? colors.outline + : colors.primary } /> @@ -308,7 +311,7 @@ const VLMScreen: React.FC = () => { @@ -326,204 +329,205 @@ const VLMScreen: React.FC = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - }, - mainContent: { - flex: 1, - }, +const createStyles = (colors: ColorScheme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + mainContent: { + flex: 1, + }, - // Model Required Overlay - modelRequiredOverlay: { - ...StyleSheet.absoluteFill, - backgroundColor: Colors.overlayMedium, - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: Padding.padding24, - zIndex: 100, - }, - modelRequiredIcon: { - marginBottom: Spacing.large, - }, - modelRequiredTitle: { - ...Typography.title2, - color: Colors.textWhite, - marginBottom: Spacing.small, - textAlign: 'center', - }, - modelRequiredSubtitle: { - ...Typography.body, - color: Colors.textSecondary, - textAlign: 'center', - marginBottom: Spacing.xLarge, - }, - selectModelButton: { - backgroundColor: Colors.primaryOrange, - paddingHorizontal: Padding.padding24, - paddingVertical: Padding.padding12, - borderRadius: BorderRadius.medium, - }, - selectModelButtonText: { - ...Typography.headline, - color: Colors.textWhite, - }, + // Model Required Overlay (always-dark scrim, so text stays literal white) + modelRequiredOverlay: { + ...StyleSheet.absoluteFill, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 24, + zIndex: 100, + }, + modelRequiredIcon: { + marginBottom: 16, + }, + modelRequiredTitle: { + ...typography.titleLarge, + color: '#FFFFFF', + marginBottom: 6, + textAlign: 'center', + }, + modelRequiredSubtitle: { + ...typography.bodyLarge, + color: 'rgba(255, 255, 255, 0.7)', + textAlign: 'center', + marginBottom: 20, + }, + selectModelButton: { + backgroundColor: colors.primary, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 10, + }, + selectModelButtonText: { + ...typography.titleMedium, + color: colors.onPrimary, + }, - // Camera Preview - cameraPreview: { - backgroundColor: Colors.backgroundPrimary, - position: 'relative', - }, - cameraPermissionView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: Colors.backgroundSecondary, - }, - cameraPermissionIcon: { - marginBottom: Spacing.medium, - }, - cameraPermissionTitle: { - ...Typography.headline, - color: Colors.textPrimary, - marginBottom: Spacing.medium, - }, - openSettingsButton: { - backgroundColor: Colors.primaryBlue, - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding8, - borderRadius: BorderRadius.regular, - }, - openSettingsButtonText: { - ...Typography.body, - color: Colors.textWhite, - }, + // Camera Preview + cameraPreview: { + backgroundColor: colors.background, + position: 'relative', + }, + cameraPermissionView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.surfaceContainer, + }, + cameraPermissionIcon: { + marginBottom: 10, + }, + cameraPermissionTitle: { + ...typography.titleMedium, + color: colors.onSurface, + marginBottom: 10, + }, + openSettingsButton: { + backgroundColor: colors.primary, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + }, + openSettingsButtonText: { + ...typography.bodyLarge, + color: colors.onPrimary, + }, - // Processing Overlay - processingOverlay: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - justifyContent: 'flex-end', - alignItems: 'center', - paddingBottom: Spacing.large, - }, - processingContent: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.6)', - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding12, - borderRadius: BorderRadius.pill, - }, - processingText: { - ...Typography.caption, - color: Colors.textWhite, - marginLeft: Spacing.smallMedium, - }, + // Processing Overlay + processingOverlay: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + justifyContent: 'flex-end', + alignItems: 'center', + paddingBottom: 16, + }, + processingContent: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.6)', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 20, + }, + processingText: { + ...typography.bodySmall, + color: '#FFFFFF', + marginLeft: 8, + }, - // Description Panel - descriptionPanel: { - flex: 1, - backgroundColor: Colors.backgroundPrimary, - paddingHorizontal: Padding.padding16, - paddingVertical: Padding.padding14, - }, - descriptionHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: Spacing.mediumLarge, - }, - descriptionTitleRow: { - flexDirection: 'row', - alignItems: 'center', - }, - descriptionTitle: { - ...Typography.headline, - color: Colors.textPrimary, - }, - liveBadge: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: `${Colors.statusGreen}20`, - paddingHorizontal: Spacing.smallMedium, - paddingVertical: Spacing.xxSmall, - borderRadius: BorderRadius.small, - marginLeft: Spacing.small, - }, - liveDot: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: Colors.statusGreen, - marginRight: Spacing.xSmall, - }, - liveText: { - ...Typography.caption2, - color: Colors.statusGreen, - fontWeight: '700', - }, - errorBanner: { - backgroundColor: Colors.badgeRed, - padding: Spacing.smallMedium, - borderRadius: BorderRadius.regular, - marginBottom: Spacing.medium, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - errorText: { - ...Typography.caption, - color: Colors.primaryRed, - flex: 1, - }, - errorDismissIcon: { - marginLeft: Spacing.small, - }, - descriptionScroll: { - flex: 1, - }, - descriptionScrollContent: { - flexGrow: 1, - }, - descriptionText: { - ...Typography.body, - color: Colors.textPrimary, - lineHeight: 22, - }, - descriptionPlaceholder: { - color: Colors.textSecondary, - }, + // Description Panel + descriptionPanel: { + flex: 1, + backgroundColor: colors.background, + paddingHorizontal: 16, + paddingVertical: 14, + }, + descriptionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + }, + descriptionTitleRow: { + flexDirection: 'row', + alignItems: 'center', + }, + descriptionTitle: { + ...typography.titleMedium, + color: colors.onSurface, + }, + liveBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: `${colors.success}20`, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + marginLeft: 6, + }, + liveDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.success, + marginRight: 4, + }, + liveText: { + ...typography.labelSmall, + color: colors.success, + fontWeight: '700', + }, + errorBanner: { + backgroundColor: colors.errorContainer, + padding: 8, + borderRadius: 8, + marginBottom: 10, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + errorText: { + ...typography.bodySmall, + color: colors.onErrorContainer, + flex: 1, + }, + errorDismissIcon: { + marginLeft: 6, + }, + descriptionScroll: { + flex: 1, + }, + descriptionScrollContent: { + flexGrow: 1, + }, + descriptionText: { + ...typography.bodyLarge, + color: colors.onSurface, + lineHeight: 22, + }, + descriptionPlaceholder: { + color: colors.onSurfaceVariant, + }, - // Control Bar - controlBar: { - flexDirection: 'row', - justifyContent: 'space-evenly', - alignItems: 'center', - backgroundColor: Colors.backgroundPrimary, - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: Colors.borderLight, - paddingVertical: Spacing.large, - paddingHorizontal: Padding.padding16, - }, - controlButton: { - padding: Spacing.medium, - }, - mainActionButton: { - width: 64, - height: 64, - borderRadius: 32, - justifyContent: 'center', - alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, -}); + // Control Bar + controlBar: { + flexDirection: 'row', + justifyContent: 'space-evenly', + alignItems: 'center', + backgroundColor: colors.background, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: colors.outlineVariant, + paddingVertical: 16, + paddingHorizontal: 16, + }, + controlButton: { + padding: 10, + }, + mainActionButton: { + width: 64, + height: 64, + borderRadius: 32, + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + }); export default VLMScreen; diff --git a/examples/react-native/RunAnywhereAI/src/theme/colors.ts b/examples/react-native/RunAnywhereAI/src/theme/colors.ts deleted file mode 100644 index 6b60fcdf6a..0000000000 --- a/examples/react-native/RunAnywhereAI/src/theme/colors.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Color System - Matching iOS AppColors.swift - * - * Reference: examples/ios/RunAnywhereAI/RunAnywhereAI/Design/AppColors.swift - */ - -export const Colors = { - // Primary Colors - primaryAccent: '#007AFF', - primaryBlue: '#007AFF', - primaryGreen: '#34C759', - primaryRed: '#FF3B30', - primaryOrange: '#FF9500', - primaryPurple: '#AF52DE', - - // Text Colors - textPrimary: '#000000', - textSecondary: '#8E8E93', - textTertiary: '#C7C7CC', - textWhite: '#FFFFFF', - - // Background Colors - Light Mode - backgroundPrimary: '#FFFFFF', - backgroundSecondary: '#F2F2F7', - backgroundTertiary: '#FFFFFF', - backgroundGrouped: '#F2F2F7', - backgroundGray5: '#E5E5EA', - backgroundGray6: '#F2F2F7', - - // Component Badges - badgeBlue: 'rgba(0, 122, 255, 0.12)', - badgeGreen: 'rgba(52, 199, 89, 0.12)', - badgePurple: 'rgba(175, 82, 222, 0.12)', - badgeOrange: 'rgba(255, 149, 0, 0.12)', - badgeRed: 'rgba(255, 59, 48, 0.12)', - badgeGray: 'rgba(142, 142, 147, 0.12)', - - // Status Colors - statusGreen: '#34C759', - statusOrange: '#FF9500', - statusRed: '#FF3B30', - statusGray: '#8E8E93', - statusBlue: '#007AFF', - - // Shadows & Overlays - shadowLight: 'rgba(0, 0, 0, 0.04)', - shadowMedium: 'rgba(0, 0, 0, 0.08)', - shadowDark: 'rgba(0, 0, 0, 0.15)', - overlayLight: 'rgba(0, 0, 0, 0.3)', - overlayMedium: 'rgba(0, 0, 0, 0.5)', - - // Borders - borderLight: 'rgba(60, 60, 67, 0.12)', - borderMedium: 'rgba(60, 60, 67, 0.29)', - - // Message Bubbles - userBubbleGradientStart: '#007AFF', - userBubbleGradientEnd: '#5856D6', - assistantBubbleBg: '#E5E5EA', - - // Framework-specific colors (from iOS) - frameworkLlamaCpp: '#FF6B35', - frameworkONNX: '#1E88E5', - frameworkCoreML: '#FF9500', - frameworkFoundationModels: '#AF52DE', - frameworkTFLite: '#FFC107', - frameworkPiperTTS: '#E91E63', - frameworkSystemTTS: '#8E8E93', -} as const; - -/** - * Dark mode color overrides - */ -export const DarkColors: Record = { - // Primary Colors (adjusted for dark mode) - primaryAccent: '#0A84FF', - primaryBlue: '#0A84FF', - primaryGreen: '#30D158', - primaryRed: '#FF453A', - primaryOrange: '#FF9F0A', - primaryPurple: '#BF5AF2', - - // Text Colors - textPrimary: '#FFFFFF', - textSecondary: '#8E8E93', - textTertiary: '#48484A', - - // Background Colors - Dark Mode - backgroundPrimary: '#000000', - backgroundSecondary: '#1C1C1E', - backgroundTertiary: '#2C2C2E', - backgroundGrouped: '#1C1C1E', - backgroundGray5: '#3A3A3C', - backgroundGray6: '#2C2C2E', - - // Message Bubbles - userBubbleGradientStart: '#0A84FF', - userBubbleGradientEnd: '#5E5CE6', - assistantBubbleBg: '#3A3A3C', - - // Borders - borderLight: 'rgba(84, 84, 88, 0.65)', - borderMedium: 'rgba(84, 84, 88, 0.90)', -}; - -export type ColorKey = keyof typeof Colors; diff --git a/examples/react-native/RunAnywhereAI/src/theme/index.ts b/examples/react-native/RunAnywhereAI/src/theme/index.ts deleted file mode 100644 index 4fc99cdcb9..0000000000 --- a/examples/react-native/RunAnywhereAI/src/theme/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Theme System - Unified export - * - * Reference: examples/ios/RunAnywhereAI/RunAnywhereAI/Design/ - */ - -import { Colors, DarkColors } from './colors'; -import { Typography } from './typography'; -import { - Spacing, - Padding, - IconSize, - ButtonHeight, - BorderRadius, - ShadowRadius, - AnimationDuration, - Layout, -} from './spacing'; - -export { Colors, DarkColors } from './colors'; -export type { ColorKey } from './colors'; - -export { Typography, FontWeight, fontSize } from './typography'; -export type { TypographyKey } from './typography'; - -export { - Spacing, - Padding, - IconSize, - ButtonHeight, - BorderRadius, - ShadowRadius, - AnimationDuration, - Layout, -} from './spacing'; -export type { SpacingKey, IconSizeKey } from './spacing'; - -/** - * Combined theme object for convenience - */ -export const Theme = { - colors: Colors, - darkColors: DarkColors, - typography: Typography, - spacing: Spacing, - padding: Padding, - iconSize: IconSize, - buttonHeight: ButtonHeight, - borderRadius: BorderRadius, - shadowRadius: ShadowRadius, - animationDuration: AnimationDuration, - layout: Layout, -}; diff --git a/examples/react-native/RunAnywhereAI/src/theme/spacing.ts b/examples/react-native/RunAnywhereAI/src/theme/spacing.ts deleted file mode 100644 index 3b17d1d3df..0000000000 --- a/examples/react-native/RunAnywhereAI/src/theme/spacing.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Spacing System - Matching iOS AppSpacing.swift - * - * Reference: examples/ios/RunAnywhereAI/RunAnywhereAI/Design/AppSpacing.swift - */ - -/** - * Semantic spacing values - */ -export const Spacing = { - // Extra small values - xxSmall: 2, - xSmall: 4, - small: 6, - smallMedium: 8, - - // Medium values - medium: 10, - mediumLarge: 12, - regular: 14, - - // Large values - large: 16, - xLarge: 20, - xxLarge: 30, - xxxLarge: 40, - - // Extra large values - huge: 48, - massive: 60, -} as const; - -/** - * Padding presets - */ -export const Padding = { - padding4: 4, - padding6: 6, - padding8: 8, - padding10: 10, - padding12: 12, - padding14: 14, - padding16: 16, - padding20: 20, - padding24: 24, - padding30: 30, - padding40: 40, - padding48: 48, - padding60: 60, - padding80: 80, - padding100: 100, -} as const; - -/** - * Icon sizes - */ -export const IconSize = { - small: 8, - regular: 18, - medium: 28, - large: 48, - xLarge: 60, - xxLarge: 72, - huge: 80, -} as const; - -/** - * Button heights - */ -export const ButtonHeight = { - small: 28, - regular: 44, - large: 72, -} as const; - -/** - * Corner radius values - */ -export const BorderRadius = { - small: 4, - regular: 8, - medium: 10, - large: 12, - xLarge: 16, - pill: 20, - circle: 9999, -} as const; - -/** - * Shadow radius values - */ -export const ShadowRadius = { - small: 2, - regular: 4, - medium: 6, - large: 8, - xLarge: 10, -} as const; - -/** - * Animation durations (in milliseconds) - */ -export const AnimationDuration = { - fast: 250, - regular: 300, - slow: 500, - verySlow: 600, - loop: 1000, - loopSlow: 2000, -} as const; - -/** - * Layout constants - */ -export const Layout = { - // Message bubble max width (75% of screen) - messageBubbleMaxWidth: 0.75, - - // Modal dimensions - modalMinWidth: 320, - modalIdealWidth: 400, - modalMaxWidth: 500, - - // Sheet dimensions - sheetMinHeight: 400, - sheetIdealHeight: 600, - sheetMaxHeight: 800, - - // Input heights - inputMinHeight: 44, - textAreaMinHeight: 120, -} as const; - -export type SpacingKey = keyof typeof Spacing; -export type IconSizeKey = keyof typeof IconSize; diff --git a/examples/react-native/RunAnywhereAI/src/theme/system/colors.ts b/examples/react-native/RunAnywhereAI/src/theme/system/colors.ts index 4b2dc7337b..cd32e13633 100644 --- a/examples/react-native/RunAnywhereAI/src/theme/system/colors.ts +++ b/examples/react-native/RunAnywhereAI/src/theme/system/colors.ts @@ -1,21 +1,40 @@ /** - * Color tokens — ported 1:1 from the Android example app - * (ui/theme/Color.kt + the Material3 light/dark schemes in Theme.kt). + * Color tokens — the single theme source for this app. + * + * Brand values mirror `examples/DESIGN_GUIDELINE.md` (canonical RunAnywhere + * palette): primary is brand orange `#FF6900` in BOTH light and dark schemes + * (guideline §2/§4 — "anchor lightScheme.primary and darkScheme.primary to + * '#FF6900'"). The primary tonal ramp is re-tuned around that hue (24.7°). + * Structure follows the Android example's Material3 light/dark schemes. * * `palette` holds the raw tonal ramps; `lightScheme`/`darkScheme` map them onto * Material3 semantic roles. UI code never reads `palette` directly — it reads * roles via `useTheme().colors`, so light/dark stays a single switch. */ -// Raw tonal palette (Color.kt) +/** + * Brand constants (DESIGN_GUIDELINE.md §1). `gradient` is the canonical + * logo/CTA gradient stops (135°, #FF6900 → #FB2C36) for any future gradient + * rendering — no screen draws a gradient today. + */ +export const brand = { + primary: '#FF6900', + gradientEnd: '#FB2C36', + gradient: ['#FF6900', '#FB2C36'] as const, + ink: '#10182B', + paper: '#FBFAF8', +} as const; + +// Raw tonal palette const palette = { - // Primary — Orange - primary20: '#4E1C00', - primary30: '#732B00', - primary60: '#E65500', - primary70: '#FF6D1F', - primary80: '#FFB693', - primary90: '#FFDBCA', + // Primary — brand orange ramp, re-tuned around #FF6900 (was Android-derived + // #E65500/#FF6D1F). primary60 IS the brand primary for both schemes. + primary20: '#522000', + primary30: '#7A3100', + primary60: brand.primary, + primary70: '#FF8C3A', // primary-bright lift (reserved; not a scheme role) + primary80: '#FFB98C', + primary90: '#FFDCC7', // Secondary — Warm Neutral secondary10: '#1F1A17', @@ -148,7 +167,9 @@ export const lightScheme: ColorScheme = { }; export const darkScheme: ColorScheme = { - primary: palette.primary70, + // Brand primary is #FF6900 in dark too (DESIGN_GUIDELINE.md §2). On-primary + // is the deep brand brown — ink-on-orange passes contrast; white would not. + primary: palette.primary60, onPrimary: palette.primary20, primaryContainer: palette.primary30, onPrimaryContainer: palette.primary90, @@ -175,7 +196,7 @@ export const darkScheme: ColorScheme = { surfaceContainer: palette.neutral12, surfaceContainerHigh: palette.neutral17, surfaceContainerHighest: palette.neutral22, - surfaceTint: palette.primary70, + surfaceTint: palette.primary60, outline: palette.neutralVariant60, outlineVariant: palette.neutralVariant30, inverseSurface: palette.neutral90, @@ -184,3 +205,23 @@ export const darkScheme: ColorScheme = { scrim: palette.neutral10, success: palette.green, }; + +/** + * Framework/backend badge colors — intentionally off-palette, theme-invariant + * hues that identify third-party inference frameworks (DESIGN_GUIDELINE.md + * rule 5: third-party marks stay off-palette). Ported from the retired legacy + * theme; `generic` uses the guideline's `info` blue, replacing legacy #007AFF. + */ +export const frameworkColors = { + llamaCpp: '#FF6B35', + onnx: '#1E88E5', + coreml: '#FF9500', + foundationModels: '#AF52DE', + tflite: '#FFC107', + piperTTS: '#E91E63', + systemTTS: '#8E8E93', + mlx: '#AF52DE', + executorch: '#FF9500', + picoLLM: '#34C759', + generic: '#3B82F6', +} as const; diff --git a/examples/react-native/RunAnywhereAI/src/theme/system/index.ts b/examples/react-native/RunAnywhereAI/src/theme/system/index.ts index cbe0a75df1..fcedf3f830 100644 --- a/examples/react-native/RunAnywhereAI/src/theme/system/index.ts +++ b/examples/react-native/RunAnywhereAI/src/theme/system/index.ts @@ -8,3 +8,4 @@ export * from './motion'; export * from './typography'; export * from './icons'; export * from './ThemeProvider'; +export * from './themedStyles'; diff --git a/examples/react-native/RunAnywhereAI/src/theme/system/themedStyles.ts b/examples/react-native/RunAnywhereAI/src/theme/system/themedStyles.ts new file mode 100644 index 0000000000..82cdec4a87 --- /dev/null +++ b/examples/react-native/RunAnywhereAI/src/theme/system/themedStyles.ts @@ -0,0 +1,35 @@ +/** + * useThemedStyles — build a StyleSheet from the active color scheme. + * + * Factories are declared at module level, so results are cached per + * (factory, scheme): exactly one StyleSheet per factory for light and one for + * dark, shared across all component instances. + * + * const styles = useThemedStyles(createStyles); + * ... + * const createStyles = (colors: ColorScheme) => StyleSheet.create({ ... }); + */ +import { useTheme } from './ThemeProvider'; +import type { ColorScheme } from './colors'; + +type StyleFactory = (colors: ColorScheme) => T; + +const cache = new WeakMap< + StyleFactory, + WeakMap +>(); + +export function useThemedStyles(factory: StyleFactory): T { + const { colors } = useTheme(); + let byScheme = cache.get(factory as StyleFactory); + if (!byScheme) { + byScheme = new WeakMap(); + cache.set(factory as StyleFactory, byScheme); + } + let styles = byScheme.get(colors) as T | undefined; + if (!styles) { + styles = factory(colors); + byScheme.set(colors, styles); + } + return styles; +} diff --git a/examples/react-native/RunAnywhereAI/src/theme/typography.ts b/examples/react-native/RunAnywhereAI/src/theme/typography.ts deleted file mode 100644 index 1a7899ce91..0000000000 --- a/examples/react-native/RunAnywhereAI/src/theme/typography.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Typography System - Matching iOS AppTypography.swift - * - * Reference: examples/ios/RunAnywhereAI/RunAnywhereAI/Design/AppTypography.swift - */ - -import type { TextStyle } from 'react-native'; -import { Platform } from 'react-native'; - -const fontFamily = Platform.select({ - ios: 'System', - android: 'Roboto', - default: 'System', -}); - -/** - * Font weights mapped to numeric values - */ -export const FontWeight = { - regular: '400' as const, - medium: '500' as const, - semibold: '600' as const, - bold: '700' as const, -}; - -/** - * Typography styles matching iOS system fonts - */ -export const Typography = { - // Large Title - used for primary headings - largeTitle: { - fontSize: 34, - fontWeight: FontWeight.bold, - lineHeight: 41, - letterSpacing: 0.37, - fontFamily, - } satisfies TextStyle, - - // Title - main titles - title: { - fontSize: 28, - fontWeight: FontWeight.bold, - lineHeight: 34, - letterSpacing: 0.36, - fontFamily, - } satisfies TextStyle, - - // Title 2 - secondary titles - title2: { - fontSize: 22, - fontWeight: FontWeight.bold, - lineHeight: 28, - letterSpacing: 0.35, - fontFamily, - } satisfies TextStyle, - - // Title 3 - tertiary titles - title3: { - fontSize: 20, - fontWeight: FontWeight.semibold, - lineHeight: 25, - letterSpacing: 0.38, - fontFamily, - } satisfies TextStyle, - - // Headline - section headers - headline: { - fontSize: 17, - fontWeight: FontWeight.semibold, - lineHeight: 22, - letterSpacing: -0.41, - fontFamily, - } satisfies TextStyle, - - // Body - main text content - body: { - fontSize: 17, - fontWeight: FontWeight.regular, - lineHeight: 22, - letterSpacing: -0.41, - fontFamily, - } satisfies TextStyle, - - // Callout - emphasized text - callout: { - fontSize: 16, - fontWeight: FontWeight.regular, - lineHeight: 21, - letterSpacing: -0.32, - fontFamily, - } satisfies TextStyle, - - // Subheadline - secondary text - subheadline: { - fontSize: 15, - fontWeight: FontWeight.regular, - lineHeight: 20, - letterSpacing: -0.24, - fontFamily, - } satisfies TextStyle, - - // Footnote - small text - footnote: { - fontSize: 13, - fontWeight: FontWeight.regular, - lineHeight: 18, - letterSpacing: -0.08, - fontFamily, - } satisfies TextStyle, - - // Caption - smallest readable text - caption: { - fontSize: 12, - fontWeight: FontWeight.regular, - lineHeight: 16, - letterSpacing: 0, - fontFamily, - } satisfies TextStyle, - - // Caption 2 - very small text - caption2: { - fontSize: 11, - fontWeight: FontWeight.regular, - lineHeight: 13, - letterSpacing: 0.06, - fontFamily, - } satisfies TextStyle, - - // Monospaced caption - for code/metrics - monospacedCaption: { - fontSize: 12, - fontWeight: FontWeight.bold, - lineHeight: 16, - letterSpacing: 0, - fontFamily: Platform.select({ - ios: 'Menlo', - android: 'monospace', - default: 'monospace', - }), - } satisfies TextStyle, -} as const; - -/** - * Create a text style with a specific size - */ -export function fontSize( - size: number, - weight: keyof typeof FontWeight = 'regular' -): TextStyle { - return { - fontSize: size, - fontWeight: FontWeight[weight], - fontFamily, - }; -} - -export type TypographyKey = keyof typeof Typography; diff --git a/examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts b/examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts index ff985c9c5d..aac422f2be 100644 --- a/examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts +++ b/examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts @@ -1,4 +1,4 @@ -import { Colors } from '../theme/colors'; +import { frameworkColors } from '../theme/system/colors'; import { QHexRT } from '@runanywhere/qhexrt'; import type { IconName } from '../theme/system/icons'; import { @@ -15,33 +15,33 @@ export const getFrameworkColor = ( ): string => { switch (framework) { case InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP: - return Colors.frameworkLlamaCpp; + return frameworkColors.llamaCpp; case InferenceFramework.INFERENCE_FRAMEWORK_PIPER_TTS: - return Colors.frameworkPiperTTS; + return frameworkColors.piperTTS; case InferenceFramework.INFERENCE_FRAMEWORK_FOUNDATION_MODELS: - return Colors.frameworkFoundationModels; + return frameworkColors.foundationModels; case InferenceFramework.INFERENCE_FRAMEWORK_COREML: - return Colors.frameworkCoreML; + return frameworkColors.coreml; case InferenceFramework.INFERENCE_FRAMEWORK_ONNX: - return Colors.frameworkONNX; + return frameworkColors.onnx; case InferenceFramework.INFERENCE_FRAMEWORK_SYSTEM_TTS: - return Colors.frameworkSystemTTS; + return frameworkColors.systemTTS; case InferenceFramework.INFERENCE_FRAMEWORK_TFLITE: - return Colors.frameworkTFLite; + return frameworkColors.tflite; case InferenceFramework.INFERENCE_FRAMEWORK_MLX: case InferenceFramework.INFERENCE_FRAMEWORK_QHEXRT: - return Colors.primaryPurple; + return frameworkColors.mlx; case InferenceFramework.INFERENCE_FRAMEWORK_EXECUTORCH: case InferenceFramework.INFERENCE_FRAMEWORK_MEDIAPIPE: - return Colors.primaryOrange; + return frameworkColors.executorch; case InferenceFramework.INFERENCE_FRAMEWORK_PICO_LLM: - return Colors.primaryGreen; + return frameworkColors.picoLLM; case InferenceFramework.INFERENCE_FRAMEWORK_MLC: case InferenceFramework.INFERENCE_FRAMEWORK_SHERPA: case InferenceFramework.INFERENCE_FRAMEWORK_SWIFT_TRANSFORMERS: - return Colors.primaryBlue; + return frameworkColors.generic; default: - return Colors.primaryBlue; + return frameworkColors.generic; } }; diff --git a/examples/web/RunAnywhereAI/AGENTS.md b/examples/web/RunAnywhereAI/AGENTS.md index 60f675503f..2d0ada6eb4 100644 --- a/examples/web/RunAnywhereAI/AGENTS.md +++ b/examples/web/RunAnywhereAI/AGENTS.md @@ -50,6 +50,19 @@ keep each view focused on DOM state and user-flow orchestration. toggle, treat a download as inference success, or silently label a failed backend/model as ready. +## Design System + +Brand primary is `#FF6900` (the logo orange), with the brand gradient +`linear-gradient(135deg, #FF6900, #FB2C36)`. The canonical palette, typography, +and contrast rules live in `../../DESIGN_GUIDELINE.md`; this app hand-maintains +its mirror of those values as CSS custom properties in +`src/styles/design-system.css` (the single token layer — `commons.css` and +`components.css` consume the variables). Light/dark theming works via +`:root[data-theme="light"|"dark"]` for the explicit toggle plus +`@media (prefers-color-scheme: light)` when no explicit choice was made. Do not +reintroduce the legacy `#FF5500`/`#E65500` orange or hardcode brand hexes in +views — use the tokens. + ## Commands Run from `examples/web/RunAnywhereAI/`. diff --git a/examples/web/RunAnywhereAI/index.html b/examples/web/RunAnywhereAI/index.html index 8bba2eb4ef..5c7a1dfd12 100644 --- a/examples/web/RunAnywhereAI/index.html +++ b/examples/web/RunAnywhereAI/index.html @@ -3,7 +3,7 @@ - + RunAnywhere diff --git a/examples/web/RunAnywhereAI/src/main.ts b/examples/web/RunAnywhereAI/src/main.ts index 8ced362dff..7d7bd96ef5 100644 --- a/examples/web/RunAnywhereAI/src/main.ts +++ b/examples/web/RunAnywhereAI/src/main.ts @@ -767,8 +767,8 @@ function showLoadingScreen(): void { - - + + diff --git a/examples/web/RunAnywhereAI/src/styles/components.css b/examples/web/RunAnywhereAI/src/styles/components.css index 79abd0c7d0..a7926f2728 100644 --- a/examples/web/RunAnywhereAI/src/styles/components.css +++ b/examples/web/RunAnywhereAI/src/styles/components.css @@ -471,7 +471,7 @@ .toolbar-model-btn:hover { background: var(--bg-hover); border-color: var(--color-primary); - box-shadow: 0 2px 8px rgba(255,85,0,0.12); + box-shadow: 0 2px 8px rgba(255,105,0,0.12); } .toolbar-model-btn:active { transform: scale(0.97); @@ -2383,8 +2383,8 @@ .mic-btn.listening { animation: mic-pulse 2s ease-in-out infinite; } @keyframes mic-pulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 85, 0, 0.4); } - 50% { box-shadow: 0 0 0 20px rgba(255, 85, 0, 0); } + 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 105, 0, 0.4); } + 50% { box-shadow: 0 0 0 20px rgba(255, 105, 0, 0); } } .mic-btn svg { width: 28px; height: 28px; } @@ -2996,7 +2996,7 @@ input[type="range"]::-webkit-slider-thumb { height: 64px; border-radius: 50%; border: 3px solid var(--color-orange); - background: rgba(255, 85, 0, 0.15); + background: rgba(255, 105, 0, 0.15); color: var(--color-orange); cursor: pointer; display: flex; @@ -3006,7 +3006,7 @@ input[type="range"]::-webkit-slider-thumb { flex-shrink: 0; } .vision-capture-btn:hover { - background: rgba(255, 85, 0, 0.25); + background: rgba(255, 105, 0, 0.25); transform: scale(1.05); } /** Scaled-down typing dots for the processing overlay */ diff --git a/examples/web/RunAnywhereAI/src/styles/design-system.css b/examples/web/RunAnywhereAI/src/styles/design-system.css index e4786d370c..06731d4e8d 100644 --- a/examples/web/RunAnywhereAI/src/styles/design-system.css +++ b/examples/web/RunAnywhereAI/src/styles/design-system.css @@ -1,8 +1,10 @@ /* ============================================================================= * RunAnywhere AI - Design System - * Shared brand language with the iOS/Android example apps: - * brand #FF5500 (accent fills, dark-surface primary) - * brandStrong #E65500 (interactive tone on light surfaces, hover) + * Brand palette per examples/DESIGN_GUIDELINE.md (../../../DESIGN_GUIDELINE.md), + * shared with the iOS/Android/Flutter/RN example apps: + * brand #FF6900 (logo orange — accent fills, dark-surface primary) + * brandStrong #E65E00 (interactive tone on light surfaces, hover) + * gradient linear-gradient(135deg, #FF6900, #FB2C36) (the logo gradient) * Surfaces are warm neutrals (not slate) to match the Android example's * Material palette and read as a consumer chat product. * Theming: dark is the :root default; light applies via @@ -18,11 +20,11 @@ color-scheme: dark; /* ---- Primary Accent Colors ---- */ - --color-primary: #FF5500; - --color-primary-strong: #E65500; - --color-primary-hover: #E65500; - --color-primary-light: rgba(255, 85, 0, 0.12); - --color-primary-medium: rgba(255, 85, 0, 0.22); + --color-primary: #FF6900; + --color-primary-strong: #E65E00; + --color-primary-hover: #E65E00; + --color-primary-light: rgba(255, 105, 0, 0.12); + --color-primary-medium: rgba(255, 105, 0, 0.22); --color-blue: #3B82F6; --color-green: #10B981; --color-red: #EF4444; @@ -47,18 +49,18 @@ --bg-input: #201F1D; /* ---- Message Bubble Colors ---- */ - --bubble-user-start: #FF5500; - --bubble-user-end: #E64500; + --bubble-user-start: #FF6900; + --bubble-user-end: #FB2C36; --bubble-assistant-start: #292725; --bubble-assistant-end: #2E2B29; /* ---- Thinking Mode Colors ---- */ - --thinking-bg: rgba(255, 85, 0, 0.08); - --thinking-border: rgba(255, 85, 0, 0.18); + --thinking-bg: rgba(255, 105, 0, 0.08); + --thinking-border: rgba(255, 105, 0, 0.18); --thinking-content-bg: #292725; /* ---- Badge Colors ---- */ - --badge-primary: rgba(255, 85, 0, 0.2); + --badge-primary: rgba(255, 105, 0, 0.2); --badge-blue: rgba(59, 130, 246, 0.2); --badge-green: rgba(16, 185, 129, 0.2); --badge-purple: rgba(139, 92, 246, 0.2); @@ -68,7 +70,7 @@ /* ---- Status Colors ---- */ --status-green: #10B981; - --status-orange: #FF5500; + --status-orange: #FF6900; --status-red: #EF4444; --status-gray: #757069; --status-blue: #3B82F6; @@ -83,7 +85,7 @@ /* ---- Shadow Colors & Elevation ---- */ --shadow-default: rgba(0, 0, 0, 0.25); --shadow-medium: rgba(0, 0, 0, 0.35); - --shadow-accent: rgba(255, 85, 0, 0.22); + --shadow-accent: rgba(255, 105, 0, 0.22); --elevation-sm: 0 1px 2px rgba(0, 0, 0, 0.18); --elevation-md: 0 4px 16px rgba(0, 0, 0, 0.24); --elevation-lg: 0 12px 40px rgba(0, 0, 0, 0.34); @@ -94,7 +96,7 @@ --overlay-dark: rgba(0, 0, 0, 0.7); /* ---- Focus Ring ---- */ - --focus-ring: 0 0 0 3px rgba(255, 85, 0, 0.35); + --focus-ring: 0 0 0 3px rgba(255, 105, 0, 0.35); /* ---- Typography ---- */ --font-family: 'Figtree', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; @@ -190,7 +192,7 @@ --bg-elevated: #FFFFFF; --bg-input: #F6F4F1; - --color-primary-hover: #CC4B00; + --color-primary-hover: #CC5400; --bubble-assistant-start: #EFECE8; --bubble-assistant-end: #F4F1ED; @@ -228,7 +230,7 @@ --bg-elevated: #FFFFFF; --bg-input: #F6F4F1; - --color-primary-hover: #CC4B00; + --color-primary-hover: #CC5400; --bubble-assistant-start: #EFECE8; --bubble-assistant-end: #F4F1ED; diff --git a/sdk/runanywhere-commons/exports/RACommons.exports b/sdk/runanywhere-commons/exports/RACommons.exports index 971126c498..90447e9bc8 100644 --- a/sdk/runanywhere-commons/exports/RACommons.exports +++ b/sdk/runanywhere-commons/exports/RACommons.exports @@ -1072,7 +1072,6 @@ _rac_stt_get_languages _rac_stt_initialize _rac_telemetry_batch_response_free _rac_telemetry_manager_batch_to_json -_rac_telemetry_manager_parse_response _rac_telemetry_manager_payload_to_json _rac_telemetry_payload_default _rac_telemetry_payload_free diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h index 4ca547ca58..a64684bb4d 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h @@ -204,16 +204,6 @@ RAC_API rac_result_t rac_telemetry_manager_batch_to_json(const rac_telemetry_batch_request_t* request, rac_environment_t env, char** out_json, size_t* out_length); -/** - * @brief Parse batch response from JSON - * - * @param json JSON response string - * @param out_response Output: Parsed response (caller must free) - * @return RAC_SUCCESS or error code - */ -RAC_API rac_result_t rac_telemetry_manager_parse_response( - const char* json, rac_telemetry_batch_response_t* out_response); - // ============================================================================= // DEVICE REGISTRATION // ============================================================================= From cbf23ddecd17bb58d68364c50e57f01263f20621 Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Sun, 19 Jul 2026 16:04:37 -0700 Subject: [PATCH 37/44] fix(spm): compile the new rcli net + auth/telemetry sources in the SwiftPM CLI target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RunAnywhereMLXCLI SwiftPM target lists rcli sources explicitly, but the network-driver files were never added — so the callers compiled while the definitions did not, failing swift-spm at link with undefined symbols rcli::net::register_device_callbacks / commands::register_auth / commands::register_telemetry. Add src/net/control_plane.cpp, src/commands/cmd_auth.cpp, src/commands/cmd_telemetry.cpp (src/main.cpp stays excluded — RunAnywhereMLXCLI provides its own Swift entry point). Verified: 'swift build --product RunAnywhereMLXCLI' links clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012nxQHeWG9c5SEwzp1tbqdc --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index d322680392..f5a453e7bb 100644 --- a/Package.swift +++ b/Package.swift @@ -434,6 +434,7 @@ let package = Package( sources: [ "src/app.cpp", "src/bootstrap.cpp", + "src/net/control_plane.cpp", "src/catalog/catalog.cpp", "src/catalog/model_ref.cpp", "src/commands/cmd_version.cpp", @@ -452,6 +453,8 @@ let package = Package( "src/commands/cmd_vad.cpp", "src/commands/cmd_voice.cpp", "src/commands/cmd_image.cpp", + "src/commands/cmd_auth.cpp", + "src/commands/cmd_telemetry.cpp", "src/commands/engine_options.cpp", "src/commands/model_setup.cpp", "src/config/cli_paths.cpp", From aa0b93a63b587530f5a46d6217b833ff223b499c Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 12:33:04 -0700 Subject: [PATCH 38/44] =?UTF-8?q?fix(rcli):=20finish=20rebase=20onto=20sdk?= =?UTF-8?q?-cross-fit=20=E2=80=94=20restore=20commons=20symbol,=20complete?= =?UTF-8?q?=20SPM=20sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep rac_telemetry_manager_parse_response (still present on the cross-fit base) and wire device_info/rag/bench into Package.swift so SPM matches CMake after the control-plane merge. Co-authored-by: Cursor --- Package.swift | 3 +++ sdk/runanywhere-commons/exports/RACommons.exports | 1 + .../infrastructure/telemetry/rac_telemetry_manager.h | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/Package.swift b/Package.swift index f5a453e7bb..d31a0941d4 100644 --- a/Package.swift +++ b/Package.swift @@ -453,11 +453,14 @@ let package = Package( "src/commands/cmd_vad.cpp", "src/commands/cmd_voice.cpp", "src/commands/cmd_image.cpp", + "src/commands/cmd_rag.cpp", + "src/commands/cmd_bench.cpp", "src/commands/cmd_auth.cpp", "src/commands/cmd_telemetry.cpp", "src/commands/engine_options.cpp", "src/commands/model_setup.cpp", "src/config/cli_paths.cpp", + "src/device_info.cpp", "src/io/wav_io.cpp", "src/io/image_io.cpp", "src/io/output.cpp", diff --git a/sdk/runanywhere-commons/exports/RACommons.exports b/sdk/runanywhere-commons/exports/RACommons.exports index 90447e9bc8..971126c498 100644 --- a/sdk/runanywhere-commons/exports/RACommons.exports +++ b/sdk/runanywhere-commons/exports/RACommons.exports @@ -1072,6 +1072,7 @@ _rac_stt_get_languages _rac_stt_initialize _rac_telemetry_batch_response_free _rac_telemetry_manager_batch_to_json +_rac_telemetry_manager_parse_response _rac_telemetry_manager_payload_to_json _rac_telemetry_payload_default _rac_telemetry_payload_free diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h index a64684bb4d..4ca547ca58 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h @@ -204,6 +204,16 @@ RAC_API rac_result_t rac_telemetry_manager_batch_to_json(const rac_telemetry_batch_request_t* request, rac_environment_t env, char** out_json, size_t* out_length); +/** + * @brief Parse batch response from JSON + * + * @param json JSON response string + * @param out_response Output: Parsed response (caller must free) + * @return RAC_SUCCESS or error code + */ +RAC_API rac_result_t rac_telemetry_manager_parse_response( + const char* json, rac_telemetry_batch_response_t* out_response); + // ============================================================================= // DEVICE REGISTRATION // ============================================================================= From 30a4611431eb710ac5cfe07bf393fac266c7f987 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 12:38:00 -0700 Subject: [PATCH 39/44] fix(rcli): use RUNANYWHERE_ENVIRONMENT as the only env name Drop the short RUNANYWHERE_ENV alias introduced by the control-plane PR so rcli matches the cross-fit integration line and live telemetry test. Co-authored-by: Cursor --- sdk/runanywhere-cli/README.md | 15 +++++++-------- sdk/runanywhere-cli/src/app.cpp | 4 +--- sdk/runanywhere-cli/src/bootstrap.cpp | 6 +++--- sdk/runanywhere-cli/src/bootstrap.h | 7 +++---- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/sdk/runanywhere-cli/README.md b/sdk/runanywhere-cli/README.md index 90d8993493..f5ab13668b 100644 --- a/sdk/runanywhere-cli/README.md +++ b/sdk/runanywhere-cli/README.md @@ -94,14 +94,13 @@ rcli can drive any RunAnywhere control plane — including a local backend on | Flag | Env var | Meaning | |---|---|---| -| `--environment ` | `RUNANYWHERE_ENV` | `dev` (default) is offline — no control plane. `staging` allows `http://` and localhost URLs. `prod` requires `https://` and rejects localhost | -| `--base-url ` | `RUNANYWHERE_BASE_URL` | Backend origin, e.g. `https://api.runanywhere.ai` or `http://127.0.0.1:8000` | -| `--api-key ` | `RUNANYWHERE_API_KEY` | Control-plane API key (≥ 10 chars), required for staging/prod | - -Combos are validated client-side before any network call: staging/prod -require both a key and a URL; passing credentials while in dev mode is an -error. With no flags at all, every command behaves exactly as before -(offline development mode). +| `--environment ` | `RUNANYWHERE_ENVIRONMENT` | `dev` (default) is offline — no control plane. `staging` allows keyless + `http://`/localhost. `prod` requires `https://` and rejects localhost | +| `--base-url ` | `RUNANYWHERE_BASE_URL` | Backend origin, e.g. `https://api.runanywhere.ai` or `http://127.0.0.1:8000` (optional on staging when the baked URL is present) | +| `--api-key ` | `RUNANYWHERE_API_KEY` | Control-plane API key (≥ 10 chars); optional on staging (keyless), required for prod | + +Combos are validated client-side before any network call. Passing credentials +while in dev mode is an error. With no flags at all, every command behaves +exactly as before (offline development mode). ```console $ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY auth login diff --git a/sdk/runanywhere-cli/src/app.cpp b/sdk/runanywhere-cli/src/app.cpp index 9f2704bd13..ff068332f5 100644 --- a/sdk/runanywhere-cli/src/app.cpp +++ b/sdk/runanywhere-cli/src/app.cpp @@ -30,12 +30,10 @@ void configure_app(CLI::App& app, GlobalOptions& options) { // Control-plane connection. Absent flags keep the historical offline // development-mode defaults; validation happens in resolve_connection(). - // RUNANYWHERE_ENV is the CLI11 envname; resolve_connection() also accepts - // RUNANYWHERE_ENVIRONMENT so scripts written for the cross-fit branch work. app.add_option("--environment", options.environment, "Control-plane environment: dev (default, offline), staging " "(keyless OK; http + localhost allowed) or prod (https only)") - ->envname("RUNANYWHERE_ENV") + ->envname("RUNANYWHERE_ENVIRONMENT") ->check(CLI::IsMember({"dev", "development", "staging", "prod", "production"})); app.add_option("--base-url", options.base_url, "Control-plane base URL, e.g. https://api.runanywhere.ai or " diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index bf80831b2a..dbcabecf8a 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -456,12 +456,12 @@ void initialize_telemetry_auth(const Connection &connection) { rac_result_t resolve_connection(const GlobalOptions &options, Connection *out, std::string *error) { - // Prefer explicit flags / CLI11 envname (RUNANYWHERE_ENV). Fall back to the - // base-branch alias RUNANYWHERE_ENVIRONMENT so existing scripts keep working. + // Prefer explicit --environment; otherwise CLI11 / getenv via + // RUNANYWHERE_ENVIRONMENT (the single canonical name on the cross-fit line). std::string environment_name = options.environment; if (environment_name.empty()) { environment_name = - first_env_value("RUNANYWHERE_ENVIRONMENT", "RUNANYWHERE_ENV", nullptr); + first_env_value("RUNANYWHERE_ENVIRONMENT", nullptr, nullptr); } std::string base_url = options.base_url; if (base_url.empty()) { diff --git a/sdk/runanywhere-cli/src/bootstrap.h b/sdk/runanywhere-cli/src/bootstrap.h index 65f5c84f56..b4d0887ca3 100644 --- a/sdk/runanywhere-cli/src/bootstrap.h +++ b/sdk/runanywhere-cli/src/bootstrap.h @@ -34,9 +34,8 @@ struct GlobalOptions { // Control-plane connection. Empty defaults preserve the historical // offline development-mode behavior exactly. CLI11 fills these from // --base-url/--api-key/--environment with RUNANYWHERE_BASE_URL / - // RUNANYWHERE_API_KEY / RUNANYWHERE_ENV env-var fallbacks (app.cpp). - // resolve_connection() also accepts RUNANYWHERE_ENVIRONMENT as an alias - // for the environment name (keyless staging remains valid). + // RUNANYWHERE_API_KEY / RUNANYWHERE_ENVIRONMENT env-var fallbacks (app.cpp). + // Staging may omit key+URL (keyless / baked staging URL). std::string environment; // dev|development|staging|prod|production ("" → dev) std::string base_url; // staging may omit (baked URL); prod requires https std::string api_key; // staging may omit (keyless); prod requires ≥10 chars @@ -64,7 +63,7 @@ struct Connection { * - staging: keyless OK (baked staging URL / PUBLIC-org); optional key+URL. * - prod: api key + https base URL required; localhost rejected. * - * Env aliases: RUNANYWHERE_ENV (CLI11) and RUNANYWHERE_ENVIRONMENT (fallback). + * Env: RUNANYWHERE_ENVIRONMENT (also --environment). */ rac_result_t resolve_connection(const GlobalOptions& options, Connection* out, std::string* error); From cf6afeb2d2095f382ee86c6eba3685b4ebf4762d Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 18:08:46 -0700 Subject: [PATCH 40/44] updates --- .github/workflows/oss-keyless-telemetry.yml | 50 ++++++ .github/workflows/release.yml | 10 +- .../runanywhereai/RunAnywhereApplication.kt | 7 +- idl/model_types.proto | 14 +- idl/sdk_init.proto | 22 +-- scripts/ci/oss_keyless_telemetry_blast.sh | 101 +++++++++++ sdk/runanywhere-cli/README.md | 62 ++++--- sdk/runanywhere-cli/src/app.cpp | 17 +- sdk/runanywhere-cli/src/bootstrap.cpp | 80 ++++----- sdk/runanywhere-cli/src/bootstrap.h | 19 +- sdk/runanywhere-cli/src/commands/cmd_auth.cpp | 3 +- .../src/commands/cmd_telemetry.cpp | 37 ++-- sdk/runanywhere-cli/src/net/control_plane.cpp | 6 +- sdk/runanywhere-cli/src/net/control_plane.h | 5 +- .../exports/RACommons.exports | 1 + .../include/rac/core/rac_sdk_state.h | 2 +- .../infrastructure/network/rac_dev_config.h | 6 +- .../infrastructure/network/rac_environment.h | 46 +++-- sdk/runanywhere-commons/src/core/rac_core.cpp | 7 - .../src/core/sdk_state.cpp | 10 +- .../src/generated/proto/model_types.pb.cc | 167 +++++++++--------- .../src/generated/proto/model_types.pb.h | 3 +- .../src/generated/proto/sdk_init.pb.cc | 20 +-- .../src/generated/proto/sdk_init.pb.h | 3 +- .../network/development_config.cpp.template | 16 +- .../infrastructure/network/environment.cpp | 131 +++++++------- .../src/jni/runanywhere_commons_jni.cpp | 2 +- .../src/lifecycle/sdk_init.cpp | 92 +++++----- sdk/runanywhere-commons/tests/CMakeLists.txt | 16 +- ....cpp => test_development_keyless_live.cpp} | 66 ++++--- .../integration/lifecycle.integration.test.js | 4 +- sdk/runanywhere-flutter/AGENTS.md | 2 +- sdk/runanywhere-flutter/README.md | 5 +- sdk/runanywhere-flutter/docs/Documentation.md | 5 +- .../lib/core/native/rac_native.dart | 2 +- .../lib/foundation/logging/sdk_logger.dart | 6 +- .../generated/convenience/ra_convenience.dart | 4 - .../runanywhere/lib/generated/logging.pb.dart | 2 +- .../lib/generated/model_types.pbenum.dart | 13 +- .../lib/generated/sdk_init.pbenum.dart | 29 +-- .../runanywhere/lib/native/dart_bridge.dart | 6 +- .../lib/native/dart_bridge_environment.dart | 6 - .../lib/native/dart_bridge_http.dart | 2 - .../lib/native/dart_bridge_state.dart | 1 - .../lib/native/dart_bridge_telemetry.dart | 2 - .../public/configuration/sdk_environment.dart | 13 +- .../runanywhere/lib/public/runanywhere.dart | 3 +- .../bridge/extensions/CppBridgeEnvironment.kt | 4 +- .../bridge/extensions/CppBridgeSdkInit.kt | 2 - .../proto/v1/LoggingConfiguration.kt | 2 +- .../ai/runanywhere/proto/v1/SDKEnvironment.kt | 17 +- .../proto/v1/SdkInitEnvironment.kt | 25 +-- .../generated/convenience/RAConvenience.kt | 2 - .../sdk/infrastructure/logging/SDKLogger.kt | 7 +- .../sdk/native/bridge/RunAnywhereBridge.kt | 6 +- .../public/configuration/SDKEnvironment.kt | 23 +-- .../core/cpp/HybridRunAnywhereCore.cpp | 13 +- .../packages/core/cpp/bridges/InitBridge.cpp | 2 +- .../core/cpp/bridges/TelemetryBridge.cpp | 2 +- .../Logging/Models/LoggingConfiguration.ts | 8 +- .../Public/Helpers/SDKEnvironment+Helpers.ts | 63 +------ .../packages/core/src/Public/RunAnywhere.ts | 4 - .../packages/core/src/types/models.ts | 2 +- sdk/runanywhere-swift/ARCHITECTURE.md | 10 +- sdk/runanywhere-swift/README.md | 1 - .../CRACommons/include/rac_dev_config.h | 6 +- .../Extensions/CppBridge+Environment.swift | 2 - .../Bridge/Extensions/CppBridge+SdkInit.swift | 1 - .../RunAnywhere/Generated/RAConvenience.swift | 2 - .../Generated/model_types.pb.swift | 16 +- .../RunAnywhere/Generated/sdk_init.pb.swift | 24 +-- .../Infrastructure/Logging/SDKLogger.swift | 6 +- .../Public/Configuration/SDKEnvironment.swift | 28 +-- .../RunAnywhere/Public/RunAnywhere.swift | 2 +- .../src/Foundation/SDKEnvironment+Helpers.ts | 79 +-------- .../packages/core/src/Foundation/SDKLogger.ts | 4 +- .../packages/core/src/Public/RunAnywhere.ts | 2 - .../convenience/model_types_convenience.js | 4 - sdk/shared/proto-ts/dist/model_types.d.ts | 11 +- sdk/shared/proto-ts/dist/model_types.js | 16 +- sdk/shared/proto-ts/dist/sdk_init.d.ts | 19 +- sdk/shared/proto-ts/dist/sdk_init.js | 24 +-- .../convenience/model_types_convenience.ts | 4 - sdk/shared/proto-ts/src/model_types.ts | 16 +- sdk/shared/proto-ts/src/sdk_init.ts | 24 +-- 85 files changed, 715 insertions(+), 894 deletions(-) create mode 100644 .github/workflows/oss-keyless-telemetry.yml create mode 100755 scripts/ci/oss_keyless_telemetry_blast.sh rename sdk/runanywhere-commons/tests/{test_staging_keyless_live.cpp => test_development_keyless_live.cpp} (70%) diff --git a/.github/workflows/oss-keyless-telemetry.yml b/.github/workflows/oss-keyless-telemetry.yml new file mode 100644 index 0000000000..91eda1d7b1 --- /dev/null +++ b/.github/workflows/oss-keyless-telemetry.yml @@ -0,0 +1,50 @@ +# ============================================================================= +# OSS keyless telemetry gate — rcli development → public staging backend +# ============================================================================= +# Primary CI for the open-source contract: build rcli in this public repo and +# keyless-blast all 12 modalities at the staging backend (PUBLIC org). No API +# key. Staging origin comes from repo secrets/vars — not hardcoded hosts. +# ============================================================================= + +name: OSS keyless telemetry + +on: + schedule: + # Daily 08:00 UTC — not on PRs/pushes (avoids burning macos runners every merge). + - cron: "0 8 * * *" + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-scheduled + cancel-in-progress: true + +permissions: + contents: read + +jobs: + keyless-staging-blast: + name: rcli keyless → staging backend + runs-on: macos-14 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + + - name: Install ninja + protobuf + run: brew install ninja protobuf + + - name: Require staging backend origin + env: + STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} + RA_OSS_BASE_URL: ${{ vars.RA_OSS_BASE_URL }} + run: | + set -euo pipefail + if [[ -z "${STAGING_BASE_URL:-}" && -z "${RA_OSS_BASE_URL:-}" ]]; then + echo "::error::Set repository secret STAGING_BASE_URL (or variable RA_OSS_BASE_URL) to the public staging backend origin." + exit 1 + fi + + - name: Build rcli + keyless blast (12 modalities) + env: + STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} + RA_OSS_BASE_URL: ${{ vars.RA_OSS_BASE_URL || secrets.STAGING_BASE_URL }} + run: bash scripts/ci/oss_keyless_telemetry_blast.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a2a31c8ec..2403134456 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,14 +38,12 @@ permissions: env: RELEASE_VERSION: ${{ github.event.inputs.version || github.ref_name }} - # Neutral backend base URL baked into rac_commons at build time — lets - # environment=staging run keyless with no explicit URL (see + # Neutral public staging backend base URL baked into rac_commons at build time + # — lets environment=development run keyless with no explicit URL (see # rac_dev_config_get_staging_base_url). Kept in a CI secret (never in the public # source tree); the commons CMake substitutes it into the generated - # development_config.cpp when present (else a placeholder stub). This is a - # neutral vanity host (e.g. api.runanywhere.ai) — no credentials, project refs, - # or tokens are ever embedded. See sdk/runanywhere-commons/CMakeLists.txt - # (DEVELOPMENT CONFIG SOURCE). + # development_config.cpp when present (else a placeholder stub). No credentials, + # project refs, or tokens are ever embedded. STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} jobs: diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt index 6f07e43940..0e5b2cf9db 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt @@ -103,11 +103,8 @@ class RunAnywhereApplication : Application() { val hasBackendConfig = BuildConfig.RUNANYWHERE_API_KEY.isNotBlank() && BuildConfig.RUNANYWHERE_BASE_URL.isNotBlank() - // Staging test build: keyless staging — no API key, no URL; the SDK - // resolves the baked staging backend URL and sends unauthenticated - // telemetry (PUBLIC-org ingestion). Restore the config-driven - // selection below to go back to production/development behavior. - //val environment = SDKEnvironment.SDK_ENVIRONMENT_STAGING + // No API key → development (keyless OSS → baked staging backend / + // PUBLIC org). With key+URL → production (org-scoped JWT path). val environment = if (hasBackendConfig) { SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION } else { diff --git a/idl/model_types.proto b/idl/model_types.proto index ff2e613091..52d1e9f4dc 100644 --- a/idl/model_types.proto +++ b/idl/model_types.proto @@ -130,19 +130,17 @@ enum ModelCategory { } // --------------------------------------------------------------------------- -// SDK environment. Sources pre-IDL: -// Swift SDKEnvironment.swift:5 (development, staging, production) -// Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) -// Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate -// Dart sdk_environment.dart:5 (development, staging, production) -// RN enums.ts:11 (Development, Staging, Production) -// Web enums.ts:9 (Development, Staging, Production) +// SDK environment — product surface is development + production only. +// Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values +// never shift PRODUCTION=3. // --------------------------------------------------------------------------- enum SDKEnvironment { SDK_ENVIRONMENT_UNSPECIFIED = 0 [(runanywhere.v1.rac_wire_string) = "unspecified"]; SDK_ENVIRONMENT_DEVELOPMENT = 1 [(runanywhere.v1.rac_wire_string) = "development"]; - SDK_ENVIRONMENT_STAGING = 2 [(runanywhere.v1.rac_wire_string) = "staging"]; SDK_ENVIRONMENT_PRODUCTION = 3 [(runanywhere.v1.rac_wire_string) = "production"]; + + reserved 2; + reserved "SDK_ENVIRONMENT_STAGING"; } // --------------------------------------------------------------------------- diff --git a/idl/sdk_init.proto b/idl/sdk_init.proto index d29ddd9d37..0bb380e89b 100644 --- a/idl/sdk_init.proto +++ b/idl/sdk_init.proto @@ -52,26 +52,16 @@ enum SdkInitPhase { // --------------------------------------------------------------------------- // Environment values — must match RAC_ENV_* in // sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h -// (development=0, staging=1, production=2). Numeric values are part of the -// wire format; do not reorder. -// -// The prior attempt to -// add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke -// Swift iOS at runtime — the shipped librac_commons.a in -// sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the -// original 0/1/2 layout, so Swift sending the regenerated enum value 1 -// (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed -// validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other -// SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped -// layout either, so reverting to the original 0/1/2 wire-format restores -// cross-SDK consistency without requiring a coordinated xcframework rebuild. -// Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of -// every prebuilt commons binary AND regeneration of all five SDK bindings. +// (development=0, production=2). Numeric values are part of the wire format; +// do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is +// reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). // --------------------------------------------------------------------------- enum SdkInitEnvironment { SDK_INIT_ENVIRONMENT_DEVELOPMENT = 0; - SDK_INIT_ENVIRONMENT_STAGING = 1; SDK_INIT_ENVIRONMENT_PRODUCTION = 2; + + reserved 1; + reserved "SDK_INIT_ENVIRONMENT_STAGING"; } // --------------------------------------------------------------------------- diff --git a/scripts/ci/oss_keyless_telemetry_blast.sh b/scripts/ci/oss_keyless_telemetry_blast.sh new file mode 100755 index 0000000000..9d6674501e --- /dev/null +++ b/scripts/ci/oss_keyless_telemetry_blast.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# OSS keyless gate: build (optional) rcli → development → public staging backend blast. +# No API key. Asserts exit 0 and all 12 modalities stored ≥ 1. +# +# Requires a staging backend origin via env (never hardcode private infra hosts): +# STAGING_BASE_URL or RA_OSS_BASE_URL +# +# STAGING_BASE_URL=https://staging.example.com ./scripts/ci/oss_keyless_telemetry_blast.sh +# RA_SKIP_BUILD=1 STAGING_BASE_URL=... ./scripts/ci/oss_keyless_telemetry_blast.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +OSS_URL="${RA_OSS_BASE_URL:-${STAGING_BASE_URL:-}}" +if [[ -z "$OSS_URL" ]]; then + echo "Set STAGING_BASE_URL or RA_OSS_BASE_URL to the public staging backend origin." >&2 + exit 1 +fi +export STAGING_BASE_URL="${STAGING_BASE_URL:-$OSS_URL}" + +case "$(uname -s)" in + Darwin) PRESET="rcli-macos-release"; JOBS="$(sysctl -n hw.logicalcpu)" ;; + Linux) PRESET="rcli-linux-release"; JOBS="$(nproc)" ;; + *) + echo "unsupported host OS '$(uname -s)'" >&2 + exit 1 + ;; +esac + +RCLI="${RA_RCLI_BIN:-$ROOT/build/$PRESET/sdk/runanywhere-cli/rcli}" + +if [[ "${RA_SKIP_BUILD:-0}" != "1" ]]; then + if [[ ! -d "$ROOT/build/$PRESET" ]]; then + cmake --preset "$PRESET" + else + # Reconfigure so STAGING_BASE_URL is injected into generated development_config.cpp + cmake --preset "$PRESET" + fi + cmake --build "build/$PRESET" --target rcli -j "$JOBS" +fi + +[[ -x "$RCLI" ]] || { + echo "rcli not executable: $RCLI" >&2 + exit 1 +} + +SESSION="${RA_OSS_SESSION_ID:-oss-ci-$(date +%s)-$RANDOM}" +TMP_HOME="$(mktemp -d "${TMPDIR:-/tmp}/oss-keyless.XXXXXX")" +cleanup() { rm -rf "$TMP_HOME"; } +trap cleanup EXIT + +# Ambient shell keys must not force JWT on the OSS path. +unset RUNANYWHERE_API_KEY RUNANYWHERE_API_URL RUNANYWHERE_ENVIRONMENT RUNANYWHERE_BASE_URL || true + +export XDG_CONFIG_HOME="$TMP_HOME/config" +export XDG_DATA_HOME="$TMP_HOME/data" +export XDG_STATE_HOME="$TMP_HOME/state" +export RUNANYWHERE_HOME="$TMP_HOME/home" + +echo "[oss-keyless] rcli=$RCLI" +echo "[oss-keyless] base_url=$OSS_URL" +echo "[oss-keyless] session_id=$SESSION" + +set +e +OUT="$("$RCLI" --environment development \ + --base-url "$OSS_URL" \ + telemetry blast \ + --processing-ms 42.5 \ + --session-id "$SESSION" \ + --input-tokens 128 \ + --output-tokens 256 2>&1)" +RC=$? +set -e +printf '%s\n' "$OUT" + +if [[ "$RC" -ne 0 ]]; then + echo "[oss-keyless] FAIL: rcli exited $RC" >&2 + exit "$RC" +fi + +MODALITIES=(llm stt tts vlm rag imagegen embeddings vad voice lora model system) +missing=0 +for m in "${MODALITIES[@]}"; do + if ! printf '%s\n' "$OUT" | awk -v mod="$m" ' + $1 == mod && $2 == "ok" { + for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) nums[++n] = $i + if (n >= 3 && nums[n-1] + 0 >= 1) { found = 1; exit } + } + END { exit found ? 0 : 1 } + '; then + echo "[oss-keyless] FAIL: modality '$m' not ok / stored < 1" >&2 + missing=1 + fi +done + +if [[ "$missing" -ne 0 ]]; then + exit 1 +fi + +echo "[oss-keyless] OK — 12/12 modalities stored (session_id=$SESSION)" diff --git a/sdk/runanywhere-cli/README.md b/sdk/runanywhere-cli/README.md index f5ab13668b..39c1c2f4f7 100644 --- a/sdk/runanywhere-cli/README.md +++ b/sdk/runanywhere-cli/README.md @@ -89,44 +89,50 @@ Exit codes: `0` ok · `1` runtime error · `2` usage error · `130` cancelled. ## Control plane -rcli can drive any RunAnywhere control plane — including a local backend on -`http://localhost` — with three global flags (each with an env-var fallback): +Two SDK environments: + +| Who | Flags | Auth | Backend | +|---|---|---|---| +| OSS / no key | `--environment development` (default) | Keyless | Baked staging backend → PUBLIC org | +| Team testing | `--environment production --base-url --api-key $KEY` | JWT | Your team backend | +| Customers | `--environment production --api-key $KEY` | JWT | Production backend | | Flag | Env var | Meaning | |---|---|---| -| `--environment ` | `RUNANYWHERE_ENVIRONMENT` | `dev` (default) is offline — no control plane. `staging` allows keyless + `http://`/localhost. `prod` requires `https://` and rejects localhost | -| `--base-url ` | `RUNANYWHERE_BASE_URL` | Backend origin, e.g. `https://api.runanywhere.ai` or `http://127.0.0.1:8000` (optional on staging when the baked URL is present) | -| `--api-key ` | `RUNANYWHERE_API_KEY` | Control-plane API key (≥ 10 chars); optional on staging (keyless), required for prod | - -Combos are validated client-side before any network call. Passing credentials -while in dev mode is an error. With no flags at all, every command behaves -exactly as before (offline development mode). +| `--environment ` | `RUNANYWHERE_ENVIRONMENT` | `development` (default) = keyless OSS telemetry. `production` = API key + https. | +| `--base-url ` | `RUNANYWHERE_BASE_URL` | Optional in development (baked staging URL in release builds). Required https for production. | +| `--api-key ` | `RUNANYWHERE_API_KEY` | Required for production (≥ 10 chars). Omit for keyless development. | ```console -$ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY auth login -organization 293beb67-… -device e87d77a2-… -token expires 2026-07-19T08:44:31Z -device row registered -assignments 0 model(s) - -$ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY telemetry blast +# OSS keyless blast → staging backend (PUBLIC org) +# Unset ambient RUNANYWHERE_API_KEY or an invalid key will force a failed JWT login. +$ unset RUNANYWHERE_API_KEY RUNANYWHERE_BASE_URL +$ rcli --environment development \ + --base-url "$STAGING_BASE_URL" \ + telemetry blast --processing-ms 42.5 +# Release builds can omit --base-url (baked STAGING_BASE_URL). +# CI gate: STAGING_BASE_URL=… ./scripts/ci/oss_keyless_telemetry_blast.sh + +# Team / customer authed path +$ rcli --environment production \ + --base-url https://api.example.com \ + --api-key $KEY auth login + +$ rcli --environment production \ + --base-url https://api.example.com \ + --api-key $KEY telemetry blast MODALITY RESULT STATUS RECEIVED STORED SKIPPED llm ok HTTP 200 1 1 0 … (one row per modality, 12 total) ``` -- `auth login` runs the same handshake the mobile SDKs run - (`/api/v1/auth/sdk/authenticate` → `/api/v1/devices/register` → - model assignments) and exits non-zero with the server's error surfaced when - anything fails. -- `telemetry emit|blast` drive the real commons telemetry pipeline: payloads - are batched per modality and POSTed to `/api/v2/sdk/telemetry/{modality}` - with the JWT from the login handshake. The V2 endpoints require a JWT, so - both commands log in first — one process performs login + emit (the token - is held in-process, not persisted). Modalities: `llm stt tts vlm rag - imagegen embeddings vad voice lora model system`. Exit is non-zero when any - POST fails or any tracked event never reached the backend. +- `auth login` runs the authenticated handshake (`/api/v1/auth/sdk/authenticate` + → `/api/v1/devices/register` → model assignments). Production only. +- `telemetry emit|blast` drive the real commons telemetry pipeline to + `/api/v2/sdk/telemetry/{modality}`. Development is keyless (no JWT). + Production logs in first. Modalities: `llm stt tts vlm rag imagegen + embeddings vad voice lora model system`. Exit is non-zero when any POST + fails or any tracked event never reached the backend. ### `rcli run` REPL diff --git a/sdk/runanywhere-cli/src/app.cpp b/sdk/runanywhere-cli/src/app.cpp index ff068332f5..361a32cb75 100644 --- a/sdk/runanywhere-cli/src/app.cpp +++ b/sdk/runanywhere-cli/src/app.cpp @@ -28,18 +28,19 @@ void configure_app(CLI::App& app, GlobalOptions& options) { "RunAnywhere home directory (default: $RUNANYWHERE_HOME or " "~/.local/share/runanywhere; models live under /Models)"); - // Control-plane connection. Absent flags keep the historical offline - // development-mode defaults; validation happens in resolve_connection(). + // Control-plane connection. validation happens in resolve_connection(). app.add_option("--environment", options.environment, - "Control-plane environment: dev (default, offline), staging " - "(keyless OK; http + localhost allowed) or prod (https only)") + "SDK environment: development (default, keyless OSS → baked staging " + "backend) or production (API key + https URL).") ->envname("RUNANYWHERE_ENVIRONMENT") - ->check(CLI::IsMember({"dev", "development", "staging", "prod", "production"})); + ->check(CLI::IsMember({"dev", "development", "prod", "production"})); app.add_option("--base-url", options.base_url, - "Control-plane base URL, e.g. https://api.runanywhere.ai or " - "http://localhost:8000 (staging/prod)") + "Backend base URL. Optional in development (baked staging URL). " + "Required https for production.") ->envname("RUNANYWHERE_BASE_URL"); - app.add_option("--api-key", options.api_key, "Control-plane API key (staging/prod)") + app.add_option("--api-key", options.api_key, + "Control-plane API key (required for production; omit for " + "keyless development)") ->envname("RUNANYWHERE_API_KEY"); commands::register_version(app, options); diff --git a/sdk/runanywhere-cli/src/bootstrap.cpp b/sdk/runanywhere-cli/src/bootstrap.cpp index dbcabecf8a..bdda07e2bb 100644 --- a/sdk/runanywhere-cli/src/bootstrap.cpp +++ b/sdk/runanywhere-cli/src/bootstrap.cpp @@ -16,6 +16,7 @@ #include "rac/desktop/rac_desktop.h" #include "rac/infrastructure/device/rac_device_identity.h" #include "rac/infrastructure/model_management/rac_model_paths.h" +#include "rac/infrastructure/network/rac_dev_config.h" #include "rac/infrastructure/network/rac_environment.h" #include "rac/infrastructure/network/rac_auth_manager.h" #include "rac/infrastructure/network/rac_endpoints.h" @@ -173,10 +174,6 @@ bool parse_environment_name(const std::string &name, rac_environment_t *out) { *out = RAC_ENV_DEVELOPMENT; return true; } - if (name == "staging") { - *out = RAC_ENV_STAGING; - return true; - } if (name == "prod" || name == "production") { *out = RAC_ENV_PRODUCTION; return true; @@ -186,11 +183,9 @@ bool parse_environment_name(const std::string &name, rac_environment_t *out) { ::runanywhere::v1::SdkInitEnvironment proto_environment_from_rac(rac_environment_t env) { - switch (env) { + switch (rac_env_normalize(env)) { case RAC_ENV_PRODUCTION: return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_PRODUCTION; - case RAC_ENV_STAGING: - return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_STAGING; default: return ::runanywhere::v1::SDK_INIT_ENVIRONMENT_DEVELOPMENT; } @@ -212,9 +207,19 @@ void initialize_sdk_metadata(const Connection &connection) { // Mirror rac_sdk_init_phase1_proto's step order: runtime state first (the // auth / device-registration / telemetry paths read env + credentials from // rac_state), then the copied SDK configuration + client info. + // Development fills the baked staging backend URL when base_url is empty. + std::string effective_base_url = connection.base_url; + if (connection.environment == RAC_ENV_DEVELOPMENT && + effective_base_url.empty()) { + const char *baked = rac_dev_config_get_staging_base_url(); + if (rac_dev_config_is_usable_http_url(baked)) { + effective_base_url = baked; + } + } + const rac_result_t state_rc = rac_state_initialize( connection.environment, connection.api_key.c_str(), - connection.base_url.c_str(), device_id[0] != '\0' ? device_id : ""); + effective_base_url.c_str(), device_id[0] != '\0' ? device_id : ""); if (state_rc != RAC_SUCCESS) { out::status_line("warning: SDK state init failed: " + out::describe_result(state_rc)); @@ -223,7 +228,7 @@ void initialize_sdk_metadata(const Connection &connection) { rac_sdk_config_t sdk_config = {}; sdk_config.environment = connection.environment; sdk_config.api_key = connection.api_key.c_str(); - sdk_config.base_url = connection.base_url.c_str(); + sdk_config.base_url = effective_base_url.c_str(); sdk_config.device_id = device_id[0] != '\0' ? device_id : ""; sdk_config.platform = desktop_platform(); sdk_config.sdk_version = RCLI_VERSION; @@ -340,27 +345,32 @@ void rcli_telemetry_http_callback(void *user_data, const char *endpoint, rac_http_response_free(&response); } -// Runs the canonical two-phase SDK init so rcli authenticates and telemetry -// actually flushes. Phase 1 sets environment + credentials; Phase 2 -// authenticates, registers the device, and enables the telemetry sink. -// Connection values come from --environment/--base-url/--api-key (or their -// RUNANYWHERE_* env fallbacks). Development mode stays fully offline. -// Staging allows keyless clients (PUBLIC-org ingestion via baked staging URL). +// Runs the canonical two-phase SDK init so telemetry can flush. +// Development (keyless OSS): Phase 1 fills baked staging backend URL when +// needed; Phase 2 skips JWT/register; telemetry POSTs anonymously → PUBLIC org. +// Production: Phase 2 authenticates + registers with the API key. void initialize_telemetry_auth(const Connection &connection) { - if (connection.environment == RAC_ENV_DEVELOPMENT) { - return; // Local offline mode — no auth, no telemetry. + const bool keyless_dev = connection.environment == RAC_ENV_DEVELOPMENT; + std::string effective_base_url = connection.base_url; + if (keyless_dev && effective_base_url.empty()) { + const char *baked = rac_dev_config_get_staging_base_url(); + if (rac_dev_config_is_usable_http_url(baked)) { + effective_base_url = baked; + } } - // Keyless staging is valid: the baked staging URL resolves in commons and - // telemetry flushes unauthenticated (PUBLIC-org ingestion). - const bool staging_keyless = connection.environment == RAC_ENV_STAGING; - if ((connection.api_key.empty() || connection.base_url.empty()) && - !staging_keyless) { + // No remote telemetry without a base URL (baked or explicit). + if (effective_base_url.empty()) { + return; + } + // Authenticated environments still need an API key. + if (!keyless_dev && connection.api_key.empty()) { return; } // Enable the auth manager. NULL secure storage: tokens are not persisted - // across runs (fine for a CLI session); authentication still runs per run. + // across runs (fine for a CLI session); authentication still runs per run + // when Phase 2 expects a key. rac_auth_init(nullptr); char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; @@ -385,7 +395,7 @@ void initialize_telemetry_auth(const Connection &connection) { ::runanywhere::v1::SdkInitPhase1Request phase1; phase1.set_environment(proto_environment_from_rac(connection.environment)); phase1.set_api_key(connection.api_key); - phase1.set_base_url(connection.base_url); + phase1.set_base_url(effective_base_url); if (device_id[0] != '\0') { phase1.set_device_id(device_id); } @@ -476,30 +486,16 @@ rac_result_t resolve_connection(const GlobalOptions &options, Connection *out, if (!parse_environment_name(environment_name, &connection.environment)) { if (error) { *error = "invalid --environment '" + environment_name + - "' (expected dev, staging or prod)"; + "' (expected development or production)"; } return RAC_ERROR_INVALID_CONFIGURATION; } connection.base_url = std::move(base_url); connection.api_key = std::move(api_key); - if (connection.environment == RAC_ENV_DEVELOPMENT) { - if (!connection.api_key.empty() || !connection.base_url.empty()) { - if (error) { - *error = "development mode (the default) has no control plane; pass " - "--environment staging (or prod) together with --base-url " - "and --api-key (staging may omit both for keyless mode)"; - } - return RAC_ERROR_INVALID_CONFIGURATION; - } - if (out) { - *out = connection; - } - return RAC_SUCCESS; - } - - // Staging accepts empty api key + empty URL (baked staging URL / keyless). - // Production still requires a real key + https URL via commons validators. + // Development (keyless OSS): optional --base-url (else baked staging backend + // URL). API key is optional and usually omitted. + // Production: API key + https base URL required (validators enforce). const rac_validation_result_t key_rc = rac_validate_api_key( connection.api_key.empty() ? nullptr : connection.api_key.c_str(), connection.environment); diff --git a/sdk/runanywhere-cli/src/bootstrap.h b/sdk/runanywhere-cli/src/bootstrap.h index b4d0887ca3..fc6202cc18 100644 --- a/sdk/runanywhere-cli/src/bootstrap.h +++ b/sdk/runanywhere-cli/src/bootstrap.h @@ -31,14 +31,14 @@ struct GlobalOptions { bool no_progress = false; std::string home_override; // --home flag - // Control-plane connection. Empty defaults preserve the historical - // offline development-mode behavior exactly. CLI11 fills these from + // Control-plane connection. CLI11 fills these from // --base-url/--api-key/--environment with RUNANYWHERE_BASE_URL / // RUNANYWHERE_API_KEY / RUNANYWHERE_ENVIRONMENT env-var fallbacks (app.cpp). - // Staging may omit key+URL (keyless / baked staging URL). - std::string environment; // dev|development|staging|prod|production ("" → dev) - std::string base_url; // staging may omit (baked URL); prod requires https - std::string api_key; // staging may omit (keyless); prod requires ≥10 chars + // development: keyless OSS → staging backend (baked URL or --base-url). + // production: API key + https URL. + std::string environment; // dev|development|prod|production ("" → dev) + std::string base_url; // development may omit (baked Staging URL) + std::string api_key; // required for production; omit for keyless development }; /** @@ -58,10 +58,9 @@ struct Connection { * RAC_ERROR_INVALID_CONFIGURATION. * * Rules (mirrors commons rac_validate_api_key / rac_validate_base_url): - * - dev (default): no credentials allowed — pass --environment staging to - * target a real control plane (localhost is allowed on staging). - * - staging: keyless OK (baked staging URL / PUBLIC-org); optional key+URL. - * - prod: api key + https base URL required; localhost rejected. + * - development (default): keyless PUBLIC-org telemetry; optional --base-url + * (else baked staging backend URL). No JWT. + * - production: API key + https base URL required; localhost rejected. * * Env: RUNANYWHERE_ENVIRONMENT (also --environment). */ diff --git a/sdk/runanywhere-cli/src/commands/cmd_auth.cpp b/sdk/runanywhere-cli/src/commands/cmd_auth.cpp index 33e7bd1a2b..0f513db7ae 100644 --- a/sdk/runanywhere-cli/src/commands/cmd_auth.cpp +++ b/sdk/runanywhere-cli/src/commands/cmd_auth.cpp @@ -110,7 +110,8 @@ void register_auth(CLI::App& app, GlobalOptions& options) { "login", "Authenticate against the configured backend (API key → JWT), register " "this device and fetch model assignments. Requires --environment " - "staging|prod with --base-url and --api-key (or RUNANYWHERE_* env vars)."); + "production with --base-url and --api-key (or RUNANYWHERE_* env vars). " + "Keyless development has no login path."); login_cmd->callback([&options]() { const int exit_code = run_auth_login(options); if (exit_code != 0) { diff --git a/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp b/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp index e2f6c3d418..a5f14bc7c7 100644 --- a/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp +++ b/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp @@ -5,12 +5,12 @@ * Drives the real commons telemetry pipeline end-to-end: payloads are queued * with rac_telemetry_manager_track, batched + serialized by commons * (one POST per modality to /api/v2/sdk/telemetry/{modality}), and delivered - * through the CLI's HTTP callback over the registered curl transport with the - * JWT from the login handshake. + * through the CLI's HTTP callback over the registered curl transport. * - * Staging/production only (the V2 endpoints require a JWT); both commands run - * the login handshake first, so one process does login + emit. Exits non-zero - * when any POST fails or any tracked event never reached the backend. + * Development (keyless): no JWT — anonymous POST → staging backend PUBLIC org. + * Production: login handshake first (API key → JWT), then flush. + * Exits non-zero when any POST fails or any tracked event never reached the + * backend. */ #include "commands/commands.h" @@ -27,6 +27,7 @@ #include "rac/core/rac_platform_adapter.h" #include "rac/core/rac_sdk_state.h" +#include "rac/infrastructure/network/rac_environment.h" #include "rac/infrastructure/telemetry/rac_telemetry_manager.h" #include "rac/infrastructure/telemetry/rac_telemetry_types.h" @@ -218,12 +219,15 @@ bool run_telemetry_session(const GlobalOptions& options, FlushReport* report, Tr return false; } - // The V2 telemetry endpoints only accept a JWT, so emit implies login — - // one process performs the handshake and the flush (in-process token). - std::string error; - if (net::login(nullptr, &error) != RAC_SUCCESS) { - out::error_line(error); - return false; + // Authenticated environments need a JWT before flush. Keyless development + // posts anonymously to staging backend (PUBLIC org) — skip login. + const rac_environment_t sdk_env = rac_state_get_environment(); + if (rac_env_auth_expected(sdk_env, rac_state_get_api_key())) { + std::string error; + if (net::login(nullptr, &error) != RAC_SUCCESS) { + out::error_line(error); + return false; + } } const char* device_id = rac_state_get_device_id(); @@ -368,7 +372,8 @@ int run_telemetry_blast(const GlobalOptions& options, int count, const std::stri received = stats.received; stored = stats.stored; skipped = stats.skipped; - row_ok = stats.failures == 0 && stats.received == count; + row_ok = stats.failures == 0 && stats.last_status == 200 && + stats.received >= count && stats.stored >= count; status = row_ok ? ("HTTP " + std::to_string(stats.last_status)) : (stats.last_error.empty() ? "HTTP " + std::to_string(stats.last_status) @@ -418,8 +423,8 @@ void register_telemetry(CLI::App& app, GlobalOptions& options) { CLI::App* emit_cmd = cmd->add_subcommand( "emit", "Track N events of one modality, flush to /api/v2/sdk/telemetry/{modality} " - "and report the backend's accounting. Runs the auth handshake first " - "(staging/prod only). Exits non-zero when any POST fails."); + "and report the backend's accounting. Production runs the auth handshake " + "first; development is keyless. Exits non-zero when any POST fails."); auto modality = std::make_shared(); auto event_type = std::make_shared(); auto count = std::make_shared(1); @@ -466,6 +471,10 @@ void register_telemetry(CLI::App& app, GlobalOptions& options) { "Session id attached to every event (default: fresh UUID)"); blast_cmd->add_option("--processing-ms", blast_metrics->processing_ms, "processing_time_ms metric for every event"); + blast_cmd->add_option("--input-tokens", blast_metrics->input_tokens, + "input_tokens metric (llm/vlm modalities)"); + blast_cmd->add_option("--output-tokens", blast_metrics->output_tokens, + "output_tokens metric (llm/vlm modalities)"); blast_cmd->callback([&options, blast_count, blast_session, blast_metrics]() { const int exit_code = run_telemetry_blast(options, *blast_count, *blast_session, *blast_metrics); diff --git a/sdk/runanywhere-cli/src/net/control_plane.cpp b/sdk/runanywhere-cli/src/net/control_plane.cpp index d74f0d410f..a8818e7a96 100644 --- a/sdk/runanywhere-cli/src/net/control_plane.cpp +++ b/sdk/runanywhere-cli/src/net/control_plane.cpp @@ -354,11 +354,11 @@ HttpResult control_plane_post(const std::string& endpoint, const std::string& js rac_result_t login(LoginSummary* out, std::string* error) { const rac_environment_t env = rac_state_get_environment(); - if (!rac_env_requires_auth(env)) { + if (!rac_env_auth_expected(env, rac_state_get_api_key())) { if (error != nullptr) { *error = - "development mode (the default) has no control plane; pass " - "--environment staging (or prod) together with --base-url and --api-key"; + "keyless development has no JWT login; use --environment production " + "with --base-url and --api-key"; } return RAC_ERROR_INVALID_CONFIGURATION; } diff --git a/sdk/runanywhere-cli/src/net/control_plane.h b/sdk/runanywhere-cli/src/net/control_plane.h index ac2dafce26..13e6633193 100644 --- a/sdk/runanywhere-cli/src/net/control_plane.h +++ b/sdk/runanywhere-cli/src/net/control_plane.h @@ -80,8 +80,9 @@ struct LoginSummary { * 2. rac_sdk_init_phase2_proto (device registration + model-assignment * fetch through the commons lifecycle orchestrator). * - * Requires a staging/production environment (development mode has no control - * plane). Idempotent within a process — a valid token short-circuits step 1. + * Requires production (or deprecated staging alias) with an API key. + * Keyless development has no JWT path — use telemetry emit/blast instead. + * Idempotent within a process — a valid token short-circuits step 1. * On failure returns a non-SUCCESS code and fills `error` with the * server-surfaced message (HTTP status + response body). */ diff --git a/sdk/runanywhere-commons/exports/RACommons.exports b/sdk/runanywhere-commons/exports/RACommons.exports index 971126c498..e8b1404b18 100644 --- a/sdk/runanywhere-commons/exports/RACommons.exports +++ b/sdk/runanywhere-commons/exports/RACommons.exports @@ -485,6 +485,7 @@ _rac_env_default_log_level _rac_env_description _rac_env_is_production _rac_env_is_testing +_rac_env_normalize _rac_env_requires_auth _rac_env_requires_backend_url _rac_env_should_send_telemetry diff --git a/sdk/runanywhere-commons/include/rac/core/rac_sdk_state.h b/sdk/runanywhere-commons/include/rac/core/rac_sdk_state.h index acc09634a6..88351e471f 100644 --- a/sdk/runanywhere-commons/include/rac/core/rac_sdk_state.h +++ b/sdk/runanywhere-commons/include/rac/core/rac_sdk_state.h @@ -45,7 +45,7 @@ extern "C" { * * Called during SDK initialization. Sets up environment and base config. * - * @param env The SDK environment (development, staging, production) + * @param env The SDK environment (development, production) * @param api_key The API key (copied internally) * @param base_url The base URL (copied internally) * @param device_id The persistent device ID (copied internally) diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h index 44224b8c89..497f19e4fb 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_dev_config.h @@ -31,9 +31,9 @@ extern "C" { /** * @brief Get the baked staging backend base URL * - * Team builds bake the staging URL via the git-ignored development_config.cpp - * so callers can init with environment=staging and nothing else. Open-source - * builds keep the placeholder and must pass a base URL explicitly. + * Release/CI bakes the staging backend URL (STAGING_BASE_URL secret) so + * environment=development can init keyless with no explicit URL. Open-source + * local builds keep the placeholder and may pass --base-url explicitly. * * @return URL string or placeholder (static, do not free) */ diff --git a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h index a103b60391..e3ef6b04a8 100644 --- a/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h +++ b/sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h @@ -2,8 +2,8 @@ * @file rac_environment.h * @brief SDK environment configuration * - * Defines environment types (development, staging, production) and their - * associated settings like authentication requirements, log levels, etc. + * Defines environment types (development, production) and their associated + * settings like authentication requirements, log levels, etc. * This is the canonical source of truth - platform SDKs create thin wrappers. */ @@ -27,16 +27,24 @@ extern "C" { /** * @brief SDK environment mode * - * - DEVELOPMENT: Local/testing mode, no auth required, uses Supabase - * - STAGING: Testing with real services, requires API key + URL - * - PRODUCTION: Live environment, requires API key + HTTPS URL + * Product surface is two environments only: + * - DEVELOPMENT (0): Open-source / keyless mode. No API key. Telemetry posts + * unauthenticated to the baked staging backend URL (PUBLIC org). + * - PRODUCTION (2): Authenticated control plane. API key → JWT; HTTPS URL. + * + * Numeric value 1 is reserved (former staging) so PRODUCTION stays at 2 for + * ABI/wire compatibility with shipped commons binaries. */ typedef enum { RAC_ENV_DEVELOPMENT = 0, - RAC_ENV_STAGING = 1, RAC_ENV_PRODUCTION = 2 } rac_environment_t; +/** + * @brief Map legacy wire value 1 (former staging) to PRODUCTION; else identity. + */ +RAC_API rac_environment_t rac_env_normalize(rac_environment_t env); + // Note: rac_log_level_t is defined in rac_types.h // We use the existing definition for consistency @@ -52,8 +60,8 @@ typedef enum { */ typedef struct { rac_environment_t environment; - const char* api_key; // Required for staging/production - const char* base_url; // Required for staging/production + const char* api_key; // Required for production + const char* base_url; // Required for production const char* device_id; // Set by platform (Keychain UUID, etc.) const char* platform; // "ios", "android", "flutter", etc. const char* sdk_version; // SDK version string @@ -79,16 +87,15 @@ typedef struct { /** * @brief Check if environment requires API authentication * @param env The environment to check - * @return true for staging/production, false for development + * @return true for production, false for development (keyless OSS) */ RAC_API bool rac_env_requires_auth(rac_environment_t env); /** * @brief Check whether authenticated requests are expected for this config * - * Staging accepts keyless clients: with no API key configured, requests go - * out unauthenticated and the backend attributes them to the PUBLIC org. - * Production always expects auth; development never does. + * Development is keyless (PUBLIC-org telemetry) unless the caller supplies + * an explicit API key. Production requires a key. * * @param env The environment to check * @param api_key The configured API key (may be NULL or empty) @@ -99,7 +106,8 @@ RAC_API bool rac_env_auth_expected(rac_environment_t env, const char* api_key); /** * @brief Check if environment requires a backend URL * @param env The environment to check - * @return true for staging/production, false for development + * @return true for production; development may omit URL when the baked + * OSS backend URL is present in the binary */ RAC_API bool rac_env_requires_backend_url(rac_environment_t env); @@ -113,28 +121,28 @@ RAC_API bool rac_env_is_production(rac_environment_t env); /** * @brief Check if environment is a testing environment * @param env The environment to check - * @return true for development and staging + * @return true for development (non-production) */ RAC_API bool rac_env_is_testing(rac_environment_t env); /** * @brief Get the default log level for an environment * @param env The environment - * @return DEBUG for development, INFO for staging, WARNING for production + * @return DEBUG for development, WARNING for production */ RAC_API rac_log_level_t rac_env_default_log_level(rac_environment_t env); /** * @brief Check if telemetry should be sent for this environment * @param env The environment - * @return true only for production + * @return true for development (keyless PUBLIC) and production */ RAC_API bool rac_env_should_send_telemetry(rac_environment_t env); /** - * @brief Check if environment should sync with backend + * @brief Check if environment should sync with backend (auth/register/assignments) * @param env The environment - * @return true for staging/production, false for development + * @return true for production, false for keyless development */ RAC_API bool rac_env_should_sync_with_backend(rac_environment_t env); @@ -157,7 +165,7 @@ RAC_API const char* rac_env_description(rac_environment_t env); * Returns a small value (3 000 ms) for `RAC_ENV_DEVELOPMENT` so the * device-registration / auth round-trip fails fast and the SDK can * proceed in offline-friendly DEV mode, and a generous value (30 000 ms) - * for staging / production where network reliability matters more. + * for production where network reliability matters more. * * Platforms that own the HTTP transport (Swift URLSession, Flutter * Dart HttpClient, Kotlin HttpURLConnection) are encouraged to call this diff --git a/sdk/runanywhere-commons/src/core/rac_core.cpp b/sdk/runanywhere-commons/src/core/rac_core.cpp index e4cf842051..12c1fdb9b5 100644 --- a/sdk/runanywhere-commons/src/core/rac_core.cpp +++ b/sdk/runanywhere-commons/src/core/rac_core.cpp @@ -337,13 +337,6 @@ rac_result_t rac_configure_logging(rac_environment_t environment) { RAC_LOG_INFO("RAC.Core", "Logging configured for development: stderr ON, level=DEBUG"); break; - case RAC_ENV_STAGING: - // Staging: print to C++ stderr + send to Swift - rac_logger_set_stderr_always(RAC_TRUE); - rac_logger_set_min_level(RAC_LOG_INFO); - RAC_LOG_INFO("RAC.Core", "Logging configured for staging: stderr ON, level=INFO"); - break; - case RAC_ENV_PRODUCTION: default: // Production: NO C++ stderr, only send to the platform bridge. diff --git a/sdk/runanywhere-commons/src/core/sdk_state.cpp b/sdk/runanywhere-commons/src/core/sdk_state.cpp index a73e75448f..059ad37f70 100644 --- a/sdk/runanywhere-commons/src/core/sdk_state.cpp +++ b/sdk/runanywhere-commons/src/core/sdk_state.cpp @@ -20,6 +20,7 @@ #include "rac/core/rac_sdk_state.h" #include "rac/infrastructure/events/rac_sdk_event_stream.h" #include "rac/infrastructure/network/rac_dev_config.h" +#include "rac/infrastructure/network/rac_environment.h" // ============================================================================= // Internal C++ State Class @@ -47,14 +48,11 @@ class SDKState { rac_result_t initialize(rac_environment_t env, const char* api_key, const char* base_url, const char* device_id) { std::lock_guard lock(mutex_); - environment_ = env; + environment_ = rac_env_normalize(env); api_key_ = api_key ? api_key : ""; base_url_ = base_url ? base_url : ""; - // Staging is absolute: whatever the caller passed, requests go keyless - // to the baked staging backend (git-ignored dev config / CI secret). - // Builds without the baked URL keep the caller's URL as-is. - if (env == RAC_ENV_STAGING) { - api_key_.clear(); + // Development (keyless OSS): fill baked staging backend URL when empty. + if (environment_ == RAC_ENV_DEVELOPMENT && base_url_.empty()) { const char* baked = rac_dev_config_get_staging_base_url(); if (rac_dev_config_is_usable_http_url(baked)) { base_url_ = baked; diff --git a/sdk/runanywhere-commons/src/generated/proto/model_types.pb.cc b/sdk/runanywhere-commons/src/generated/proto/model_types.pb.cc index b12e395576..283e4e3413 100644 --- a/sdk/runanywhere-commons/src/generated/proto/model_types.pb.cc +++ b/sdk/runanywhere-commons/src/generated/proto/model_types.pb.cc @@ -8964,90 +8964,89 @@ const char descriptor_table_protodef_model_5ftypes_2eproto[] ABSL_ATTRIBUTE_SECT "O\020\007\032\t\342\265\030\005audio\022+\n\030MODEL_CATEGORY_EMBEDDI" "NG\020\010\032\r\342\265\030\tembedding\022I\n\'MODEL_CATEGORY_VO" "ICE_ACTIVITY_DETECTION\020\t\032\034\342\265\030\030voice-acti" - "vity-detection*\316\001\n\016SDKEnvironment\0220\n\033SDK" + "vity-detection*\303\001\n\016SDKEnvironment\0220\n\033SDK" "_ENVIRONMENT_UNSPECIFIED\020\000\032\017\342\265\030\013unspecif" "ied\0220\n\033SDK_ENVIRONMENT_DEVELOPMENT\020\001\032\017\342\265" - "\030\013development\022(\n\027SDK_ENVIRONMENT_STAGING" - "\020\002\032\013\342\265\030\007staging\022.\n\032SDK_ENVIRONMENT_PRODU" - "CTION\020\003\032\016\342\265\030\nproduction*\255\001\n\013ModelSource\022" - "-\n\030MODEL_SOURCE_UNSPECIFIED\020\000\032\017\342\265\030\013unspe" - "cified\022#\n\023MODEL_SOURCE_REMOTE\020\001\032\n\342\265\030\006rem" - "ote\022!\n\022MODEL_SOURCE_LOCAL\020\002\032\t\342\265\030\005local\022\'" - "\n\025MODEL_SOURCE_BUILT_IN\020\003\032\014\342\265\030\010built-in*" - "\215\001\n\013ArchiveType\022\034\n\030ARCHIVE_TYPE_UNSPECIF" - "IED\020\000\022\024\n\020ARCHIVE_TYPE_ZIP\020\001\022\030\n\024ARCHIVE_T" - "YPE_TAR_BZ2\020\002\022\027\n\023ARCHIVE_TYPE_TAR_GZ\020\003\022\027" - "\n\023ARCHIVE_TYPE_TAR_XZ\020\004*\252\002\n\020ArchiveStruc" - "ture\0222\n\035ARCHIVE_STRUCTURE_UNSPECIFIED\020\000\032" - "\017\342\265\030\013unspecified\022>\n$ARCHIVE_STRUCTURE_SI" - "NGLE_FILE_NESTED\020\001\032\024\342\265\030\020singleFileNested" - "\0229\n!ARCHIVE_STRUCTURE_DIRECTORY_BASED\020\002\032" - "\022\342\265\030\016directoryBased\022;\n\"ARCHIVE_STRUCTURE" - "_NESTED_DIRECTORY\020\003\032\023\342\265\030\017nestedDirectory" - "\022*\n\031ARCHIVE_STRUCTURE_UNKNOWN\020\004\032\013\342\265\030\007unk" - "nown*\245\003\n\021ModelArtifactType\022#\n\037MODEL_ARTI" - "FACT_TYPE_UNSPECIFIED\020\000\022#\n\037MODEL_ARTIFAC" - "T_TYPE_SINGLE_FILE\020\001\022&\n\"MODEL_ARTIFACT_T" - "YPE_TAR_GZ_ARCHIVE\020\002\022!\n\035MODEL_ARTIFACT_T" - "YPE_DIRECTORY\020\003\022#\n\037MODEL_ARTIFACT_TYPE_Z" - "IP_ARCHIVE\020\004\022\036\n\032MODEL_ARTIFACT_TYPE_CUST" - "OM\020\005\022\037\n\033MODEL_ARTIFACT_TYPE_ARCHIVE\020\006\022\"\n" - "\036MODEL_ARTIFACT_TYPE_MULTI_FILE\020\007\022 \n\034MOD" - "EL_ARTIFACT_TYPE_BUILT_IN\020\010\022\'\n#MODEL_ART" - "IFACT_TYPE_TAR_BZ2_ARCHIVE\020\t\022&\n\"MODEL_AR" - "TIFACT_TYPE_TAR_XZ_ARCHIVE\020\n*\225\002\n\023ModelRe" - "gistryStatus\022%\n!MODEL_REGISTRY_STATUS_UN" - "SPECIFIED\020\000\022$\n MODEL_REGISTRY_STATUS_REG" - "ISTERED\020\001\022%\n!MODEL_REGISTRY_STATUS_DOWNL" - "OADING\020\002\022$\n MODEL_REGISTRY_STATUS_DOWNLO" - "ADED\020\003\022!\n\035MODEL_REGISTRY_STATUS_LOADING\020" - "\004\022 \n\034MODEL_REGISTRY_STATUS_LOADED\020\005\022\037\n\033M" - "ODEL_REGISTRY_STATUS_ERROR\020\006*\305\002\n\023ModelQu" - "erySortField\022&\n\"MODEL_QUERY_SORT_FIELD_U" - "NSPECIFIED\020\000\022\037\n\033MODEL_QUERY_SORT_FIELD_N" - "AME\020\001\022-\n)MODEL_QUERY_SORT_FIELD_CREATED_" - "AT_UNIX_MS\020\002\022-\n)MODEL_QUERY_SORT_FIELD_U" - "PDATED_AT_UNIX_MS\020\003\022.\n*MODEL_QUERY_SORT_" - "FIELD_DOWNLOAD_SIZE_BYTES\020\004\022/\n+MODEL_QUE" - "RY_SORT_FIELD_LAST_USED_AT_UNIX_MS\020\005\022&\n\"" - "MODEL_QUERY_SORT_FIELD_USAGE_COUNT\020\006*\212\001\n" - "\023ModelQuerySortOrder\022&\n\"MODEL_QUERY_SORT" - "_ORDER_UNSPECIFIED\020\000\022$\n MODEL_QUERY_SORT" - "_ORDER_ASCENDING\020\001\022%\n!MODEL_QUERY_SORT_O" - "RDER_DESCENDING\020\002*\253\002\n\rModelFileRole\022\037\n\033M" - "ODEL_FILE_ROLE_UNSPECIFIED\020\000\022!\n\035MODEL_FI" - "LE_ROLE_PRIMARY_MODEL\020\001\022\035\n\031MODEL_FILE_RO" - "LE_COMPANION\020\002\022$\n MODEL_FILE_ROLE_VISION" - "_PROJECTOR\020\003\022\035\n\031MODEL_FILE_ROLE_TOKENIZE" - "R\020\004\022\032\n\026MODEL_FILE_ROLE_CONFIG\020\005\022\036\n\032MODEL" - "_FILE_ROLE_VOCABULARY\020\006\022\032\n\026MODEL_FILE_RO" - "LE_MERGES\020\007\022\032\n\026MODEL_FILE_ROLE_LABELS\020\010*" - "\325\001\n\rRoutingPolicy\022\036\n\032ROUTING_POLICY_UNSP" - "ECIFIED\020\000\022\037\n\033ROUTING_POLICY_PREFER_LOCAL" - "\020\001\022\037\n\033ROUTING_POLICY_PREFER_CLOUD\020\002\022!\n\035R" - "OUTING_POLICY_COST_OPTIMIZED\020\003\022$\n ROUTIN" - "G_POLICY_LATENCY_OPTIMIZED\020\004\022\031\n\025ROUTING_" - "POLICY_MANUAL\020\0052\203\005\n\rModelRegistry\022@\n\010Reg" - "ister\022\031.runanywhere.v1.ModelInfo\032\031.runan" - "ywhere.v1.ModelInfo\022>\n\006Update\022\031.runanywh" - "ere.v1.ModelInfo\032\031.runanywhere.v1.ModelI" - "nfo\022F\n\003Get\022\037.runanywhere.v1.ModelGetRequ" - "est\032\036.runanywhere.v1.ModelGetResult\022I\n\004L" - "ist\022 .runanywhere.v1.ModelListRequest\032\037." - "runanywhere.v1.ModelListResult\022O\n\006Remove" - "\022\".runanywhere.v1.ModelDeleteRequest\032!.r" - "unanywhere.v1.ModelDeleteResult\022O\n\006Impor" - "t\022\".runanywhere.v1.ModelImportRequest\032!." - "runanywhere.v1.ModelImportResult\022W\n\010Disc" - "over\022%.runanywhere.v1.ModelDiscoveryRequ" - "est\032$.runanywhere.v1.ModelDiscoveryResul" - "t\022b\n\007Refresh\022+.runanywhere.v1.ModelRegis" - "tryRefreshRequest\032*.runanywhere.v1.Model" - "RegistryRefreshResultB\212\001\n\027ai.runanywhere" - ".proto.v1B\017ModelTypesProtoP\001Z\n$ARCHIVE_STRUCTURE_SINGLE_FILE_N" + "ESTED\020\001\032\024\342\265\030\020singleFileNested\0229\n!ARCHIVE" + "_STRUCTURE_DIRECTORY_BASED\020\002\032\022\342\265\030\016direct" + "oryBased\022;\n\"ARCHIVE_STRUCTURE_NESTED_DIR" + "ECTORY\020\003\032\023\342\265\030\017nestedDirectory\022*\n\031ARCHIVE" + "_STRUCTURE_UNKNOWN\020\004\032\013\342\265\030\007unknown*\245\003\n\021Mo" + "delArtifactType\022#\n\037MODEL_ARTIFACT_TYPE_U" + "NSPECIFIED\020\000\022#\n\037MODEL_ARTIFACT_TYPE_SING" + "LE_FILE\020\001\022&\n\"MODEL_ARTIFACT_TYPE_TAR_GZ_" + "ARCHIVE\020\002\022!\n\035MODEL_ARTIFACT_TYPE_DIRECTO" + "RY\020\003\022#\n\037MODEL_ARTIFACT_TYPE_ZIP_ARCHIVE\020" + "\004\022\036\n\032MODEL_ARTIFACT_TYPE_CUSTOM\020\005\022\037\n\033MOD" + "EL_ARTIFACT_TYPE_ARCHIVE\020\006\022\"\n\036MODEL_ARTI" + "FACT_TYPE_MULTI_FILE\020\007\022 \n\034MODEL_ARTIFACT" + "_TYPE_BUILT_IN\020\010\022\'\n#MODEL_ARTIFACT_TYPE_" + "TAR_BZ2_ARCHIVE\020\t\022&\n\"MODEL_ARTIFACT_TYPE" + "_TAR_XZ_ARCHIVE\020\n*\225\002\n\023ModelRegistryStatu" + "s\022%\n!MODEL_REGISTRY_STATUS_UNSPECIFIED\020\000" + "\022$\n MODEL_REGISTRY_STATUS_REGISTERED\020\001\022%" + "\n!MODEL_REGISTRY_STATUS_DOWNLOADING\020\002\022$\n" + " MODEL_REGISTRY_STATUS_DOWNLOADED\020\003\022!\n\035M" + "ODEL_REGISTRY_STATUS_LOADING\020\004\022 \n\034MODEL_" + "REGISTRY_STATUS_LOADED\020\005\022\037\n\033MODEL_REGIST" + "RY_STATUS_ERROR\020\006*\305\002\n\023ModelQuerySortFiel" + "d\022&\n\"MODEL_QUERY_SORT_FIELD_UNSPECIFIED\020" + "\000\022\037\n\033MODEL_QUERY_SORT_FIELD_NAME\020\001\022-\n)MO" + "DEL_QUERY_SORT_FIELD_CREATED_AT_UNIX_MS\020" + "\002\022-\n)MODEL_QUERY_SORT_FIELD_UPDATED_AT_U" + "NIX_MS\020\003\022.\n*MODEL_QUERY_SORT_FIELD_DOWNL" + "OAD_SIZE_BYTES\020\004\022/\n+MODEL_QUERY_SORT_FIE" + "LD_LAST_USED_AT_UNIX_MS\020\005\022&\n\"MODEL_QUERY" + "_SORT_FIELD_USAGE_COUNT\020\006*\212\001\n\023ModelQuery" + "SortOrder\022&\n\"MODEL_QUERY_SORT_ORDER_UNSP" + "ECIFIED\020\000\022$\n MODEL_QUERY_SORT_ORDER_ASCE" + "NDING\020\001\022%\n!MODEL_QUERY_SORT_ORDER_DESCEN" + "DING\020\002*\253\002\n\rModelFileRole\022\037\n\033MODEL_FILE_R" + "OLE_UNSPECIFIED\020\000\022!\n\035MODEL_FILE_ROLE_PRI" + "MARY_MODEL\020\001\022\035\n\031MODEL_FILE_ROLE_COMPANIO" + "N\020\002\022$\n MODEL_FILE_ROLE_VISION_PROJECTOR\020" + "\003\022\035\n\031MODEL_FILE_ROLE_TOKENIZER\020\004\022\032\n\026MODE" + "L_FILE_ROLE_CONFIG\020\005\022\036\n\032MODEL_FILE_ROLE_" + "VOCABULARY\020\006\022\032\n\026MODEL_FILE_ROLE_MERGES\020\007" + "\022\032\n\026MODEL_FILE_ROLE_LABELS\020\010*\325\001\n\rRouting" + "Policy\022\036\n\032ROUTING_POLICY_UNSPECIFIED\020\000\022\037" + "\n\033ROUTING_POLICY_PREFER_LOCAL\020\001\022\037\n\033ROUTI" + "NG_POLICY_PREFER_CLOUD\020\002\022!\n\035ROUTING_POLI" + "CY_COST_OPTIMIZED\020\003\022$\n ROUTING_POLICY_LA" + "TENCY_OPTIMIZED\020\004\022\031\n\025ROUTING_POLICY_MANU" + "AL\020\0052\203\005\n\rModelRegistry\022@\n\010Register\022\031.run" + "anywhere.v1.ModelInfo\032\031.runanywhere.v1.M" + "odelInfo\022>\n\006Update\022\031.runanywhere.v1.Mode" + "lInfo\032\031.runanywhere.v1.ModelInfo\022F\n\003Get\022" + "\037.runanywhere.v1.ModelGetRequest\032\036.runan" + "ywhere.v1.ModelGetResult\022I\n\004List\022 .runan" + "ywhere.v1.ModelListRequest\032\037.runanywhere" + ".v1.ModelListResult\022O\n\006Remove\022\".runanywh" + "ere.v1.ModelDeleteRequest\032!.runanywhere." + "v1.ModelDeleteResult\022O\n\006Import\022\".runanyw" + "here.v1.ModelImportRequest\032!.runanywhere" + ".v1.ModelImportResult\022W\n\010Discover\022%.runa" + "nywhere.v1.ModelDiscoveryRequest\032$.runan" + "ywhere.v1.ModelDiscoveryResult\022b\n\007Refres" + "h\022+.runanywhere.v1.ModelRegistryRefreshR" + "equest\032*.runanywhere.v1.ModelRegistryRef" + "reshResultB\212\001\n\027ai.runanywhere.proto.v1B\017" + "ModelTypesProtoP\001Z enum SDKEnvironment : int { SDK_ENVIRONMENT_UNSPECIFIED = 0, SDK_ENVIRONMENT_DEVELOPMENT = 1, - SDK_ENVIRONMENT_STAGING = 2, SDK_ENVIRONMENT_PRODUCTION = 3, SDKEnvironment_INT_MIN_SENTINEL_DO_NOT_USE_ = ::std::numeric_limits<::int32_t>::min(), @@ -683,7 +682,7 @@ inline constexpr SDKEnvironment SDKEnvironment_MIN = inline constexpr SDKEnvironment SDKEnvironment_MAX = static_cast(3); [[nodiscard]] inline bool SDKEnvironment_IsValid(int value) { - return 0 <= value && value <= 3; + return 0 <= value && value <= 3 && ((11u >> value) & 1) != 0; } inline constexpr int SDKEnvironment_ARRAYSIZE = 3 + 1; [[nodiscard]] const ::google::protobuf::EnumDescriptor* PROTOBUF_NONNULL diff --git a/sdk/runanywhere-commons/src/generated/proto/sdk_init.pb.cc b/sdk/runanywhere-commons/src/generated/proto/sdk_init.pb.cc index 992a5ecf51..6da6208139 100644 --- a/sdk/runanywhere-commons/src/generated/proto/sdk_init.pb.cc +++ b/sdk/runanywhere-commons/src/generated/proto/sdk_init.pb.cc @@ -738,14 +738,14 @@ const char descriptor_table_protodef_sdk_5finit_2eproto[] ABSL_ATTRIBUTE_SECTION "}\n\014SdkInitPhase\022\036\n\032SDK_INIT_PHASE_UNSPEC" "IFIED\020\000\022\026\n\022SDK_INIT_PHASE_ONE\020\001\022\026\n\022SDK_I" "NIT_PHASE_TWO\020\002\022\035\n\031SDK_INIT_PHASE_RETRY_" - "HTTP\020\003*\201\001\n\022SdkInitEnvironment\022$\n SDK_INI" - "T_ENVIRONMENT_DEVELOPMENT\020\000\022 \n\034SDK_INIT_" - "ENVIRONMENT_STAGING\020\001\022#\n\037SDK_INIT_ENVIRO" - "NMENT_PRODUCTION\020\002B\207\001\n\027ai.runanywhere.pr" - "oto.v1B\014SdkInitProtoP\001Z } enum SdkInitEnvironment : int { SDK_INIT_ENVIRONMENT_DEVELOPMENT = 0, - SDK_INIT_ENVIRONMENT_STAGING = 1, SDK_INIT_ENVIRONMENT_PRODUCTION = 2, SdkInitEnvironment_INT_MIN_SENTINEL_DO_NOT_USE_ = ::std::numeric_limits<::int32_t>::min(), @@ -158,7 +157,7 @@ inline constexpr SdkInitEnvironment SdkInitEnvironment_MIN = inline constexpr SdkInitEnvironment SdkInitEnvironment_MAX = static_cast(2); [[nodiscard]] inline bool SdkInitEnvironment_IsValid(int value) { - return 0 <= value && value <= 2; + return 0 <= value && value <= 2 && ((5u >> value) & 1) != 0; } inline constexpr int SdkInitEnvironment_ARRAYSIZE = 2 + 1; [[nodiscard]] const ::google::protobuf::EnumDescriptor* PROTOBUF_NONNULL diff --git a/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template b/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template index 0ede1bac93..2b40acdce9 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template +++ b/sdk/runanywhere-commons/src/infrastructure/network/development_config.cpp.template @@ -7,11 +7,12 @@ * 2. Fill in your development values * 3. development_config.cpp is git-ignored, so your config won't be committed * - * Only the staging backend base URL is baked here (so environment=staging can - * run keyless with no explicit URL). CI substitutes STAGING_BASE_URL from a - * secret; open-source builds keep the placeholder and pass a base URL - * explicitly. No credentials, project refs, or tokens are embedded — the SDK - * reaches the backend only through this neutral base URL. + * Only a neutral staging backend base URL is baked here (so + * environment=development can run keyless with no explicit URL). CI + * substitutes STAGING_BASE_URL from a secret; open-source builds keep the + * placeholder and pass a base URL explicitly. No credentials, project refs, + * or tokens are embedded — the SDK reaches the backend only through this + * neutral base URL. */ #include "rac/infrastructure/network/rac_dev_config.h" @@ -22,8 +23,9 @@ namespace { -// Staging backend base URL — baked into team builds so environment=staging -// needs no explicit URL. Leave the placeholder to require an explicit URL. +// Staging backend base URL — baked into release/CI builds so +// environment=development needs no explicit URL. Leave the placeholder to +// require an explicit URL for local/OSS builds without the secret. constexpr const char* STAGING_BASE_URL = "YOUR_STAGING_BASE_URL"; } // anonymous namespace diff --git a/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp b/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp index 3cd10b7038..2aae889a96 100644 --- a/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp +++ b/sdk/runanywhere-commons/src/infrastructure/network/environment.cpp @@ -60,41 +60,45 @@ struct SdkConfigSnapshotStorage { // Environment Query Functions // ============================================================================= +rac_environment_t rac_env_normalize(rac_environment_t env) { + // Former staging occupied numeric 1; accept it as production if seen on + // the wire from older clients. + if (static_cast(env) == 1) { + return RAC_ENV_PRODUCTION; + } + return env; +} + bool rac_env_requires_auth(rac_environment_t env) { - return env != RAC_ENV_DEVELOPMENT; + return rac_env_normalize(env) != RAC_ENV_DEVELOPMENT; } bool rac_env_auth_expected(rac_environment_t env, const char* api_key) { if (!rac_env_requires_auth(env)) { - // Development authenticates when the caller supplied an explicit key. + // Development is keyless OSS by default (PUBLIC-org telemetry). An + // explicit key is honored if the caller supplies one. return api_key != nullptr && api_key[0] != '\0'; } - // Staging accepts keyless clients — requests go out unauthenticated and - // the backend attributes them to the PUBLIC org. Production stays strict. - if (env == RAC_ENV_STAGING && (!api_key || api_key[0] == '\0')) { - return false; - } + // Staging is a deprecated production alias — API key required. return true; } bool rac_env_requires_backend_url(rac_environment_t env) { - return env != RAC_ENV_DEVELOPMENT; + return rac_env_normalize(env) != RAC_ENV_DEVELOPMENT; } bool rac_env_is_production(rac_environment_t env) { - return env == RAC_ENV_PRODUCTION; + return rac_env_normalize(env) == RAC_ENV_PRODUCTION; } bool rac_env_is_testing(rac_environment_t env) { - return env == RAC_ENV_DEVELOPMENT || env == RAC_ENV_STAGING; + return rac_env_normalize(env) == RAC_ENV_DEVELOPMENT; } rac_log_level_t rac_env_default_log_level(rac_environment_t env) { - switch (env) { + switch (rac_env_normalize(env)) { case RAC_ENV_DEVELOPMENT: return RAC_LOG_DEBUG; // From rac_types.h: 1 - case RAC_ENV_STAGING: - return RAC_LOG_INFO; // From rac_types.h: 2 case RAC_ENV_PRODUCTION: return RAC_LOG_WARNING; // From rac_types.h: 3 default: @@ -103,24 +107,20 @@ rac_log_level_t rac_env_default_log_level(rac_environment_t env) { } bool rac_env_should_send_telemetry(rac_environment_t env) { - // Telemetry is sent in every environment — development flushes immediately to - // the local backend, staging and production batch + send with auth. This must - // agree with the actual send gate (rac_env_requires_auth, also !=DEVELOPMENT); - // returning production-only here previously contradicted that and mislabeled - // staging as "no telemetry" even though staging does send. - return env != RAC_ENV_DEVELOPMENT; + // Development and production both send telemetry (keyless vs authed). + (void)env; + return true; } bool rac_env_should_sync_with_backend(rac_environment_t env) { - return env != RAC_ENV_DEVELOPMENT; + // Keyless development skips authenticate / device register / assignments. + return rac_env_normalize(env) != RAC_ENV_DEVELOPMENT; } const char* rac_env_description(rac_environment_t env) { - switch (env) { + switch (rac_env_normalize(env)) { case RAC_ENV_DEVELOPMENT: - return "Development Environment"; - case RAC_ENV_STAGING: - return "Staging Environment"; + return "Development Environment (keyless OSS)"; case RAC_ENV_PRODUCTION: return "Production Environment"; default: @@ -128,22 +128,12 @@ const char* rac_env_description(rac_environment_t env) { } } -// In DEV mode the SDK targets an operator- -// side DNS alias (e.g. dev.runanywhere.local). When the alias is not -// configured locally the OS resolver burns its full default timeout -// before the request can fail, blocking SDK init for ~30 s per cold -// launch. Returning a short timeout here lets platform HTTP layers -// (URLSession / Dart HttpClient / Kotlin HttpURLConnection) fail fast -// in DEV without hurting production reliability. +// Development posts keyless telemetry to staging backend (or an explicit +// base URL). Use the same generous timeout as authenticated backends — +// Staging is a real remote host, not a local DNS alias. int32_t rac_env_default_http_timeout_ms(rac_environment_t env) { - switch (env) { - case RAC_ENV_DEVELOPMENT: - return 3000; // 3s — fail fast on unreachable dev DNS - case RAC_ENV_STAGING: - case RAC_ENV_PRODUCTION: - default: - return 30000; // 30s — generous default for real backends - } + (void)env; + return 30000; } // ============================================================================= @@ -210,13 +200,12 @@ static bool is_localhost_host(const char* host) { // ============================================================================= rac_validation_result_t rac_validate_api_key(const char* api_key, rac_environment_t env) { - // Development never needs a key; staging accepts an empty one (keyless - // clients send unauthenticated requests, attributed to the PUBLIC org) + // Development keyless OSS: empty key is OK (PUBLIC-org telemetry). if (!rac_env_auth_expected(env, api_key)) { return RAC_VALIDATION_OK; } - // Production requires API key + // Production requires an API key. if (!api_key || api_key[0] == '\0') { return RAC_VALIDATION_API_KEY_REQUIRED; } @@ -230,18 +219,33 @@ rac_validation_result_t rac_validate_api_key(const char* api_key, rac_environmen } rac_validation_result_t rac_validate_base_url(const char* url, rac_environment_t env) { - // Development mode doesn't require URL - if (!rac_env_requires_backend_url(env)) { + env = rac_env_normalize(env); + // Development may omit URL when the baked staging backend URL is present. + if (env == RAC_ENV_DEVELOPMENT) { + if (!url || url[0] == '\0') { + if (rac_dev_config_is_usable_http_url(rac_dev_config_get_staging_base_url())) { + return RAC_VALIDATION_OK; + } + // No baked URL and no caller URL — still OK for purely offline use; + // telemetry simply has nowhere to go until a URL is configured. + return RAC_VALIDATION_OK; + } + char scheme[16] = {0}; + if (!extract_url_scheme(url, scheme, sizeof(scheme))) { + return RAC_VALIDATION_URL_INVALID_SCHEME; + } + if (strcmp(scheme, "https") != 0 && strcmp(scheme, "http") != 0) { + return RAC_VALIDATION_URL_INVALID_SCHEME; + } + char host[256] = {0}; + if (!extract_url_host(url, host, sizeof(host)) || host[0] == '\0') { + return RAC_VALIDATION_URL_INVALID_HOST; + } return RAC_VALIDATION_OK; } - // Staging/Production require URL — except staging builds carrying the - // baked backend URL, where an empty URL resolves to it at init + // Staging (deprecated alias) and production require an explicit URL. if (!url || url[0] == '\0') { - if (env == RAC_ENV_STAGING && - rac_dev_config_is_usable_http_url(rac_dev_config_get_staging_base_url())) { - return RAC_VALIDATION_OK; - } return RAC_VALIDATION_URL_REQUIRED; } @@ -251,16 +255,9 @@ rac_validation_result_t rac_validate_base_url(const char* url, rac_environment_t return RAC_VALIDATION_URL_INVALID_SCHEME; } - // Production requires HTTPS - if (env == RAC_ENV_PRODUCTION) { - if (strcmp(scheme, "https") != 0) { - return RAC_VALIDATION_URL_HTTPS_REQUIRED; - } - } else if (env == RAC_ENV_STAGING) { - // Staging allows HTTP or HTTPS - if (strcmp(scheme, "https") != 0 && strcmp(scheme, "http") != 0) { - return RAC_VALIDATION_URL_INVALID_SCHEME; - } + // Production and staging-alias require HTTPS (no localhost http escape hatch). + if (strcmp(scheme, "https") != 0) { + return RAC_VALIDATION_URL_HTTPS_REQUIRED; } // Extract and validate host @@ -273,8 +270,8 @@ rac_validation_result_t rac_validate_base_url(const char* url, rac_environment_t return RAC_VALIDATION_URL_INVALID_HOST; } - // Production cannot use localhost/example URLs - if (env == RAC_ENV_PRODUCTION && is_localhost_host(host)) { + // Authenticated environments cannot use localhost/example URLs + if (is_localhost_host(host)) { return RAC_VALIDATION_URL_LOCALHOST_NOT_ALLOWED; } @@ -417,12 +414,12 @@ rac_validation_result_t rac_sdk_init(const rac_sdk_config_t* config) { return RAC_VALIDATION_API_KEY_REQUIRED; } - // Staging is absolute: whatever the caller passed, requests go keyless to - // the baked staging backend (git-ignored dev config / CI secret). Builds - // without the baked URL keep the caller's URL as-is. + // Normalize reserved STAGING → PRODUCTION. Development (keyless OSS): + // fill the baked staging backend URL when the caller omitted base_url. rac_sdk_config_t effective = *config; - if (effective.environment == RAC_ENV_STAGING) { - effective.api_key = ""; + effective.environment = rac_env_normalize(effective.environment); + if (effective.environment == RAC_ENV_DEVELOPMENT && + (!effective.base_url || effective.base_url[0] == '\0')) { const char* baked = rac_dev_config_get_staging_base_url(); if (rac_dev_config_is_usable_http_url(baked)) { effective.base_url = baked; diff --git a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp index 777c25396d..d6b4a050ae 100644 --- a/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp +++ b/sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp @@ -3532,7 +3532,7 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racDevConfigIsUsableHtt * This must be called during SDK initialization for device registration * to include the correct sdk_version (instead of "unknown"). * - * @param environment Environment (0=development, 1=staging, 2=production) + * @param environment Environment (0=development, 2=production; 1 reserved) * @param deviceId Device ID string * @param platform Platform string (e.g., "android") * @param sdkVersion SDK version string (e.g., "0.1.0") diff --git a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp index ea3544d7da..c40a998e88 100644 --- a/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp +++ b/sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp @@ -64,15 +64,8 @@ using ::runanywhere::v1::SdkInitResult; // -- helpers ---------------------------------------------------------------- rac_environment_t to_rac_environment(SdkInitEnvironment env) { - switch (env) { - case ::runanywhere::v1::SDK_INIT_ENVIRONMENT_STAGING: - return RAC_ENV_STAGING; - case ::runanywhere::v1::SDK_INIT_ENVIRONMENT_PRODUCTION: - return RAC_ENV_PRODUCTION; - case ::runanywhere::v1::SDK_INIT_ENVIRONMENT_DEVELOPMENT: - default: - return RAC_ENV_DEVELOPMENT; - } + // DEVELOPMENT=0, PRODUCTION=2. Reserved former-staging (=1) → production. + return rac_env_normalize(static_cast(static_cast(env))); } rac_result_t serialize_result(const SdkInitResult& result, rac_proto_buffer_t* out) { @@ -117,8 +110,8 @@ bool http_setup_applicable_for_state() { if (!rac_dev_config_is_usable_http_url(base_url)) { return false; } - // Keyless (dev / keyless-staging) is a valid HTTP setup: unauthenticated - // public-org ingestion. Auth-required environments need a usable API key. + // Keyless development is a valid HTTP setup: unauthenticated PUBLIC-org + // ingestion. Auth-required environments need a usable API key. return rac_dev_config_is_usable_credential(api_key) || !rac_env_auth_expected(env, api_key); } @@ -318,8 +311,8 @@ rac_result_t perform_authentication(SdkInitResult* result) { return RAC_ERROR_INVALID_CONFIGURATION; } if (!rac_env_auth_expected(env, api_key)) { - // Keyless staging: requests go out unauthenticated (public ingestion), - // there is no token to fetch + // Keyless development: requests go out unauthenticated (PUBLIC org), + // there is no token to fetch. result->set_http_configured(rac_http_transport_is_registered() == RAC_TRUE); result->set_has_completed_http_setup(true); return RAC_SUCCESS; @@ -537,11 +530,9 @@ rac_result_t rac_sdk_init_phase1_proto(const uint8_t* in_request_bytes, size_t i std::string base_url = request.base_url(); const std::string device_id = request.device_id(); - // Staging is absolute: whatever the caller passed, requests go keyless to - // the baked staging backend (git-ignored dev config / CI secret). Builds - // without the baked URL keep the caller's URL as-is. - if (env == RAC_ENV_STAGING) { - api_key.clear(); + // Development (keyless OSS): fill baked staging backend URL when empty. + // Staging no longer wipes keys — it is a deprecated production alias. + if (env == RAC_ENV_DEVELOPMENT && base_url.empty()) { const char* baked = rac_dev_config_get_staging_base_url(); if (rac_dev_config_is_usable_http_url(baked)) { base_url = baked; @@ -552,7 +543,8 @@ rac_result_t rac_sdk_init_phase1_proto(const uint8_t* in_request_bytes, size_t i request.sdk_version().empty() ? std::string(rac_sdk_get_version()) : request.sdk_version(); // Step 1: Validate inputs. Staging/production require API key + URL. - if (environment_requires_external_config(env)) { + // Development validates optionally (baked URL / keyless). + if (environment_requires_external_config(env) || env == RAC_ENV_DEVELOPMENT) { const rac_validation_result_t key_check = rac_validate_api_key(api_key.empty() ? nullptr : api_key.c_str(), env); if (key_check != RAC_VALIDATION_OK) { @@ -673,42 +665,44 @@ rac_result_t rac_sdk_init_phase2_proto(const uint8_t* in_request_bytes, size_t i append_warning(&result, warning_from_code("auth setup deferred", auth_rc)); } - // Step 2: Register device only when a complete control-plane - // configuration exists. Credential-free local SDK use is not a failed - // registration attempt and must not emit an error-level platform log. + // Step 2: Register device + fetch assignments only on the JWT path. + // Keyless development posts PUBLIC-org telemetry without register/for-sdk. const rac_environment_t env = rac_state_get_environment(); - const char* build_token = - request.build_token().empty() ? nullptr : request.build_token().c_str(); - const rac_result_t dev_rc = rac_device_manager_register_if_needed(env, build_token); - const bool device_registered = - (dev_rc == RAC_SUCCESS) || (rac_device_manager_is_registered() == RAC_TRUE); - result.set_device_registered(device_registered); - if (dev_rc != RAC_SUCCESS && dev_rc != RAC_ERROR_FEATURE_NOT_AVAILABLE && - dev_rc != RAC_ERROR_NOT_INITIALIZED) { - // Surface as a warning rather than aborting — matches Swift's - // "Device registration failed (non-critical)" branch. - append_warning(&result, warning_from_code("device registration deferred", dev_rc)); + const char* api_key = rac_state_get_api_key(); + if (rac_env_auth_expected(env, api_key)) { + const char* build_token = + request.build_token().empty() ? nullptr : request.build_token().c_str(); + const rac_result_t dev_rc = rac_device_manager_register_if_needed(env, build_token); + const bool device_registered = + (dev_rc == RAC_SUCCESS) || (rac_device_manager_is_registered() == RAC_TRUE); + result.set_device_registered(device_registered); + if (dev_rc != RAC_SUCCESS && dev_rc != RAC_ERROR_FEATURE_NOT_AVAILABLE && + dev_rc != RAC_ERROR_NOT_INITIALIZED) { + // Surface as a warning rather than aborting — matches Swift's + // "Device registration failed (non-critical)" branch. + append_warning(&result, warning_from_code("device registration deferred", dev_rc)); + } + + // Step 3: Fetch model assignments (cached). + rac_model_info_t** assigned_models = nullptr; + size_t assigned_count = 0; + const rac_result_t fetch_rc = rac_model_assignment_fetch( + request.force_refresh_assignments() ? RAC_TRUE : RAC_FALSE, &assigned_models, + &assigned_count); + if (fetch_rc == RAC_SUCCESS && assigned_models != nullptr) { + result.set_linked_models_count(static_cast(assigned_count)); + rac_model_info_array_free(assigned_models, assigned_count); + } else if (fetch_rc != RAC_ERROR_FEATURE_NOT_AVAILABLE && fetch_rc != RAC_SUCCESS) { + append_warning(&result, + warning_from_code("model assignment fetch deferred", fetch_rc)); + } + } else { + result.set_device_registered(false); } } else { result.set_device_registered(rac_device_manager_is_registered() == RAC_TRUE); } - // Step 3: Fetch model assignments (cached). When callbacks are not wired - // this returns RAC_ERROR_FEATURE_NOT_AVAILABLE; we treat that as offline. - rac_model_info_t** assigned_models = nullptr; - size_t assigned_count = 0; - const rac_result_t fetch_rc = - rac_model_assignment_fetch(request.force_refresh_assignments() ? RAC_TRUE : RAC_FALSE, - &assigned_models, &assigned_count); - if (fetch_rc == RAC_SUCCESS && assigned_models != nullptr) { - result.set_linked_models_count(static_cast(assigned_count)); - rac_model_info_array_free(assigned_models, assigned_count); - } else if (fetch_rc != RAC_ERROR_FEATURE_NOT_AVAILABLE && fetch_rc != RAC_SUCCESS) { - // Non-fatal: cache may be empty and HTTP unavailable. Warning surface - // mirrors Swift's offline-mode branch. - append_warning(&result, warning_from_code("model assignment fetch deferred", fetch_rc)); - } - // Step 4: Flush telemetry via the sink registered by the SDK. if (request.flush_telemetry()) { const rac_result_t flush_rc = rac_events_flush_telemetry_sink(); diff --git a/sdk/runanywhere-commons/tests/CMakeLists.txt b/sdk/runanywhere-commons/tests/CMakeLists.txt index 9aa5526e00..a00546d030 100644 --- a/sdk/runanywhere-commons/tests/CMakeLists.txt +++ b/sdk/runanywhere-commons/tests/CMakeLists.txt @@ -1670,14 +1670,14 @@ endif() include(GoogleTest) gtest_discover_tests(rac_benchmark_tests) -# --- Keyless staging live E2E (network; build on demand, not in ctest) ------ -# Proves environment=staging with no API key and no base URL resolves the -# baked staging URL and flushes one event per modality unauthenticated. +# --- Keyless development live E2E (network; build on demand, not in ctest) --- +# Proves environment=development with no API key and no base URL resolves the +# baked OSS backend URL and flushes one event per modality unauthenticated. find_package(CURL QUIET) if(CURL_FOUND) - add_executable(test_staging_keyless_live EXCLUDE_FROM_ALL test_staging_keyless_live.cpp) - target_include_directories(test_staging_keyless_live PRIVATE ${CMAKE_SOURCE_DIR}/include) - target_link_libraries(test_staging_keyless_live PRIVATE rac_commons CURL::libcurl Threads::Threads) - rac_link_archive_deps(test_staging_keyless_live) - target_compile_features(test_staging_keyless_live PRIVATE cxx_std_17) + add_executable(test_development_keyless_live EXCLUDE_FROM_ALL test_development_keyless_live.cpp) + target_include_directories(test_development_keyless_live PRIVATE ${CMAKE_SOURCE_DIR}/include) + target_link_libraries(test_development_keyless_live PRIVATE rac_commons CURL::libcurl Threads::Threads) + rac_link_archive_deps(test_development_keyless_live) + target_compile_features(test_development_keyless_live PRIVATE cxx_std_17) endif() diff --git a/sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp b/sdk/runanywhere-commons/tests/test_development_keyless_live.cpp similarity index 70% rename from sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp rename to sdk/runanywhere-commons/tests/test_development_keyless_live.cpp index 73d137b6ee..06b4e40973 100644 --- a/sdk/runanywhere-commons/tests/test_staging_keyless_live.cpp +++ b/sdk/runanywhere-commons/tests/test_development_keyless_live.cpp @@ -1,16 +1,18 @@ -// Live E2E for keyless staging telemetry: init with environment=staging and -// nothing else (no API key, no base URL — the baked dev-config staging URL -// must resolve), emit one terminal event per modality, flush unauthenticated -// and expect the backend to store each one under the PUBLIC org. +// Live E2E for keyless development telemetry: init with +// environment=development and no API key. Base URL comes from the baked +// staging backend URL (STAGING_BASE_URL / development_config) or must be +// set via rac_state after bake. Emit one terminal event per modality, flush +// unauthenticated, and expect the backend to store each under the PUBLIC org. // -// Network test against the real staging backend — build on demand, not part +// Network test against the real Staging backend — build on demand, not part // of the ctest suite. Requires a build configured with STAGING_BASE_URL (or a -// filled local development_config.cpp). +// filled local development_config.cpp), or set RAC_LIVE_BASE_URL. #include #include #include +#include #include #include #include @@ -26,6 +28,12 @@ namespace { int g_ok = 0; int g_failed = 0; +int64_t now_ms() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + size_t discard_body(char*, size_t size, size_t nmemb, void*) { return size * nmemb; } @@ -56,33 +64,40 @@ void http_send(void*, const char* endpoint, const char* json_body, size_t json_l } std::string random_uuid() { +#if defined(__linux__) std::ifstream f("/proc/sys/kernel/random/uuid"); std::string uuid; std::getline(f, uuid); - return uuid; -} - -int64_t now_ms() { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); + if (!uuid.empty()) { + return uuid; + } +#endif + return "live-" + std::to_string(now_ms()); } } // namespace int main() { - rac_state_initialize(RAC_ENV_STAGING, "", "", "keyless-harness-device"); + const char* override_url = std::getenv("RAC_LIVE_BASE_URL"); + const char* base_arg = (override_url && override_url[0] != '\0') ? override_url : ""; + rac_state_initialize(RAC_ENV_DEVELOPMENT, "", base_arg, "keyless-harness-device"); const char* base_url = rac_state_get_base_url(); std::printf("resolved base_url: %s\n", base_url && base_url[0] ? base_url : ""); if (!base_url || base_url[0] == '\0') { - std::printf("FAIL: staging base URL did not resolve from the baked dev config\n"); + std::printf( + "FAIL: development base URL did not resolve (bake STAGING_BASE_URL " + "or set RAC_LIVE_BASE_URL)\n"); + return 1; + } + if (!rac_env_should_send_telemetry(RAC_ENV_DEVELOPMENT)) { + std::printf("FAIL: rac_env_should_send_telemetry(DEVELOPMENT) is false\n"); return 1; } curl_global_init(CURL_GLOBAL_DEFAULT); - rac_telemetry_manager_t* mgr = - rac_telemetry_manager_create(RAC_ENV_STAGING, "keyless-harness-device", "linux", "0.0.0"); + rac_telemetry_manager_t* mgr = rac_telemetry_manager_create( + RAC_ENV_DEVELOPMENT, "keyless-harness-device", "linux", "0.0.0"); rac_telemetry_manager_set_device_info(mgr, "Linux Keyless Harness", "6.12"); rac_telemetry_manager_set_http_callback(mgr, http_send, nullptr); @@ -90,15 +105,20 @@ int main() { const char* modality; const char* event_type; }; + // All 12 V2 modalities. const Case cases[] = { {"llm", "llm.generation.completed"}, {"stt", "stt.transcription.completed"}, {"tts", "tts.synthesis.completed"}, - {"vlm", "vlm.generation.completed"}, - {"rag", "rag.retrieval.completed"}, - {"imagegen", "imagegen.generation.completed"}, - {"system", "sdk.init.completed"}, + {"vlm", "vlm.process.completed"}, + {"rag", "rag.query.completed"}, + {"imagegen", "imagegen.generate.completed"}, + {"embeddings", "embeddings.embed.completed"}, + {"vad", "vad.stopped"}, + {"voice", "voice.turn.metrics"}, + {"lora", "lora.attach.completed"}, {"model", "model.download.completed"}, + {"system", "sdk.init.completed"}, }; for (const Case& c : cases) { @@ -116,7 +136,7 @@ int main() { p.os_version = "6.12"; p.platform = "linux"; p.sdk_version = "0.0.0"; - p.processing_time_ms = 123.0; + p.processing_time_ms = 42.5; p.has_processing_time_ms = RAC_TRUE; p.success = RAC_TRUE; p.has_success = RAC_TRUE; @@ -139,5 +159,5 @@ int main() { curl_global_cleanup(); std::printf("summary: ok=%d failed=%d\n", g_ok, g_failed); - return g_failed == 0 && g_ok > 0 ? 0 : 1; + return g_failed == 0 && g_ok >= 12 ? 0 : 1; } diff --git a/sdk/runanywhere-electron/test/integration/lifecycle.integration.test.js b/sdk/runanywhere-electron/test/integration/lifecycle.integration.test.js index c2a0d6e2ba..d1d60bfb74 100644 --- a/sdk/runanywhere-electron/test/integration/lifecycle.integration.test.js +++ b/sdk/runanywhere-electron/test/integration/lifecycle.integration.test.js @@ -18,9 +18,9 @@ test('two-phase init exposes ready-state and emits lifecycle + telemetry events' // Phase 1 (synchronous). assert.equal(RunAnywhere.isInitialized, false); - RunAnywhere.initialize({ environment: 'staging' }); + RunAnywhere.initialize({ environment: 'development' }); assert.equal(RunAnywhere.isInitialized, true); - assert.equal(RunAnywhere.environment, 'staging'); + assert.equal(RunAnywhere.environment, 'development'); assert.ok(seen.includes('initialized'), 'initialized event fired'); // Phase 2 (background services) — awaitable + idempotent. diff --git a/sdk/runanywhere-flutter/AGENTS.md b/sdk/runanywhere-flutter/AGENTS.md index 3fd8c58a70..30fc904f77 100644 --- a/sdk/runanywhere-flutter/AGENTS.md +++ b/sdk/runanywhere-flutter/AGENTS.md @@ -115,7 +115,7 @@ iOS requires `use_frameworks! :linkage => :static` in the Podfile and `-all_load await RunAnywhere.initialize( apiKey: 'optional', baseURL: 'optional', - environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, // or SDK_ENVIRONMENT_STAGING, SDK_ENVIRONMENT_PRODUCTION + environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, // or SDK_ENVIRONMENT_PRODUCTION ); // Capability accessors (shared capability instances) diff --git a/sdk/runanywhere-flutter/README.md b/sdk/runanywhere-flutter/README.md index c2f3138ed7..307b472f9a 100644 --- a/sdk/runanywhere-flutter/README.md +++ b/sdk/runanywhere-flutter/README.md @@ -422,9 +422,8 @@ await RunAnywhere.initialize( | Environment | Description | |-------------|-------------| -| `SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT` | Verbose logging, local-only, no auth required | -| `SDKEnvironment.SDK_ENVIRONMENT_STAGING` | Testing with real services | -| `SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION` | Minimal logging, full authentication, telemetry | +| `SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT` | Keyless OSS mode; verbose logging; no API key | +| `SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION` | Authenticated control plane; minimal logging; telemetry | ### Generation Options diff --git a/sdk/runanywhere-flutter/docs/Documentation.md b/sdk/runanywhere-flutter/docs/Documentation.md index 88e3c4eddd..94f2e2b300 100644 --- a/sdk/runanywhere-flutter/docs/Documentation.md +++ b/sdk/runanywhere-flutter/docs/Documentation.md @@ -435,7 +435,7 @@ All public API types are protobuf-generated from `idl/*.proto`. They live under | Type | Proto enum values | |------|-------------------| -| `SDKEnvironment` | `SDK_ENVIRONMENT_DEVELOPMENT`, `SDK_ENVIRONMENT_STAGING`, `SDK_ENVIRONMENT_PRODUCTION` | +| `SDKEnvironment` | `SDK_ENVIRONMENT_DEVELOPMENT`, `SDK_ENVIRONMENT_PRODUCTION` | | `InferenceFramework` | `INFERENCE_FRAMEWORK_LLAMA_CPP`, `INFERENCE_FRAMEWORK_MLX`, `INFERENCE_FRAMEWORK_SHERPA`, `INFERENCE_FRAMEWORK_ONNX`, `INFERENCE_FRAMEWORK_SYSTEM_TTS`, ... | | `ModelCategory` | `MODEL_CATEGORY_LANGUAGE`, `MODEL_CATEGORY_SPEECH_RECOGNITION`, `MODEL_CATEGORY_SPEECH_SYNTHESIS`, `MODEL_CATEGORY_MULTIMODAL`, `MODEL_CATEGORY_EMBEDDING`, `MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION`, ... | | `DownloadStage` | `DOWNLOAD_STAGE_UNSPECIFIED`, `DOWNLOAD_STAGE_DOWNLOADING`, `DOWNLOAD_STAGE_EXTRACTING`, `DOWNLOAD_STAGE_COMPLETED` | @@ -486,8 +486,7 @@ The event stream is a pure `dart:async` broadcast stream. (`rxdart` is not a dep ```dart enum SDKEnvironment { - SDK_ENVIRONMENT_DEVELOPMENT, // local-only, verbose logging, no auth - SDK_ENVIRONMENT_STAGING, // real services for testing + SDK_ENVIRONMENT_DEVELOPMENT, // keyless OSS, verbose logging, no auth SDK_ENVIRONMENT_PRODUCTION, // minimal logging, full auth, telemetry } ``` diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/core/native/rac_native.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/core/native/rac_native.dart index 07c55579c9..bee8c0d145 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/core/native/rac_native.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/core/native/rac_native.dart @@ -2469,7 +2469,7 @@ class RacBindings { /// `rac_env_is_production(rac_environment_t)` — true only for production. final RacEnvPredicateDart? rac_env_is_production; - /// `rac_env_is_testing(rac_environment_t)` — true for development/staging. + /// `rac_env_is_testing(rac_environment_t)` — true for development (non-production). final RacEnvPredicateDart? rac_env_is_testing; /// `rac_env_requires_auth(rac_environment_t)` — true for non-development. diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/logging/sdk_logger.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/logging/sdk_logger.dart index bba4f65f65..647520c283 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/logging/sdk_logger.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/logging/sdk_logger.dart @@ -29,7 +29,7 @@ import 'package:runanywhere/generated/model_types.pbenum.dart' export 'package:runanywhere/generated/logging.pbenum.dart' show LogLevel; /// Per-environment [LoggingConfiguration] presets. The generated proto message -/// cannot be `const`-constructed, so the development/staging/production presets +/// cannot be `const`-constructed, so the development/production presets /// live here as factory helpers (mirrors Swift's `RALoggingConfiguration` /// extension in `SDKLogger.swift`). class LoggingConfigurations { @@ -49,7 +49,7 @@ class LoggingConfigurations { includeDeviceMetadata: false, ); - /// Staging preset — info-level logging (matches Swift). + /// Staging preset — info-level logging profile (not an SDK environment). static LoggingConfiguration get staging => LoggingConfiguration( enableLocalLogging: true, minLogLevel: LogLevel.LOG_LEVEL_INFO, @@ -70,8 +70,6 @@ class LoggingConfigurations { switch (environment) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return development; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return staging; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return production; default: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dart index e24a64d4cb..b7ca16e26d 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dart @@ -137,8 +137,6 @@ extension SDKEnvironmentWireString on SDKEnvironment { return 'unspecified'; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'development'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'staging'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'production'; } @@ -152,8 +150,6 @@ SDKEnvironment? sdkEnvironmentFromWireString(String value) { return SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED; case 'development': return SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - case 'staging': - return SDKEnvironment.SDK_ENVIRONMENT_STAGING; case 'production': return SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart index dd2824fe97..5b6b018785 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart @@ -23,7 +23,7 @@ export 'logging.pbenum.dart'; /// --------------------------------------------------------------------------- /// SDK logging configuration. Per-environment presets -/// (development/staging/production) stay in each SDK as factory helpers. +/// (development/production) stay in each SDK as factory helpers. /// --------------------------------------------------------------------------- class LoggingConfiguration extends $pb.GeneratedMessage { factory LoggingConfiguration({ diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/model_types.pbenum.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/model_types.pbenum.dart index f219ed2ea6..fe99edb86c 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/model_types.pbenum.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/model_types.pbenum.dart @@ -291,28 +291,21 @@ class ModelCategory extends $pb.ProtobufEnum { } /// --------------------------------------------------------------------------- -/// SDK environment. Sources pre-IDL: -/// Swift SDKEnvironment.swift:5 (development, staging, production) -/// Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) -/// Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate -/// Dart sdk_environment.dart:5 (development, staging, production) -/// RN enums.ts:11 (Development, Staging, Production) -/// Web enums.ts:9 (Development, Staging, Production) +/// SDK environment — product surface is development + production only. +/// Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values +/// never shift PRODUCTION=3. /// --------------------------------------------------------------------------- class SDKEnvironment extends $pb.ProtobufEnum { static const SDKEnvironment SDK_ENVIRONMENT_UNSPECIFIED = SDKEnvironment._(0, _omitEnumNames ? '' : 'SDK_ENVIRONMENT_UNSPECIFIED'); static const SDKEnvironment SDK_ENVIRONMENT_DEVELOPMENT = SDKEnvironment._(1, _omitEnumNames ? '' : 'SDK_ENVIRONMENT_DEVELOPMENT'); - static const SDKEnvironment SDK_ENVIRONMENT_STAGING = - SDKEnvironment._(2, _omitEnumNames ? '' : 'SDK_ENVIRONMENT_STAGING'); static const SDKEnvironment SDK_ENVIRONMENT_PRODUCTION = SDKEnvironment._(3, _omitEnumNames ? '' : 'SDK_ENVIRONMENT_PRODUCTION'); static const $core.List values = [ SDK_ENVIRONMENT_UNSPECIFIED, SDK_ENVIRONMENT_DEVELOPMENT, - SDK_ENVIRONMENT_STAGING, SDK_ENVIRONMENT_PRODUCTION, ]; diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_init.pbenum.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_init.pbenum.dart index b85254766c..2bec272446 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_init.pbenum.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/sdk_init.pbenum.dart @@ -47,43 +47,26 @@ class SdkInitPhase extends $pb.ProtobufEnum { /// --------------------------------------------------------------------------- /// Environment values — must match RAC_ENV_* in /// sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h -/// (development=0, staging=1, production=2). Numeric values are part of the -/// wire format; do not reorder. -/// -/// The prior attempt to -/// add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke -/// Swift iOS at runtime — the shipped librac_commons.a in -/// sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the -/// original 0/1/2 layout, so Swift sending the regenerated enum value 1 -/// (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed -/// validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other -/// SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped -/// layout either, so reverting to the original 0/1/2 wire-format restores -/// cross-SDK consistency without requiring a coordinated xcframework rebuild. -/// Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of -/// every prebuilt commons binary AND regeneration of all five SDK bindings. +/// (development=0, production=2). Numeric values are part of the wire format; +/// do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is +/// reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). /// --------------------------------------------------------------------------- class SdkInitEnvironment extends $pb.ProtobufEnum { static const SdkInitEnvironment SDK_INIT_ENVIRONMENT_DEVELOPMENT = SdkInitEnvironment._( 0, _omitEnumNames ? '' : 'SDK_INIT_ENVIRONMENT_DEVELOPMENT'); - static const SdkInitEnvironment SDK_INIT_ENVIRONMENT_STAGING = - SdkInitEnvironment._( - 1, _omitEnumNames ? '' : 'SDK_INIT_ENVIRONMENT_STAGING'); static const SdkInitEnvironment SDK_INIT_ENVIRONMENT_PRODUCTION = SdkInitEnvironment._( 2, _omitEnumNames ? '' : 'SDK_INIT_ENVIRONMENT_PRODUCTION'); static const $core.List values = [ SDK_INIT_ENVIRONMENT_DEVELOPMENT, - SDK_INIT_ENVIRONMENT_STAGING, SDK_INIT_ENVIRONMENT_PRODUCTION, ]; - static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 2); - static SdkInitEnvironment? valueOf($core.int value) => - value < 0 || value >= _byValue.length ? null : _byValue[value]; + static final $core.Map<$core.int, SdkInitEnvironment> _byValue = + $pb.ProtobufEnum.initByValue(values); + static SdkInitEnvironment? valueOf($core.int value) => _byValue[value]; const SdkInitEnvironment._(super.value, super.name); } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart index 2f997444ff..dfe1e2c256 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart @@ -132,7 +132,7 @@ class DartBridge { /// /// Call this FIRST during SDK init. Must complete before Phase 2. /// - /// [environment] The SDK environment (development/staging/production) + /// [environment] The SDK environment (development/production) /// [apiKey] Resolved API key for production/staging, or empty in development. /// [baseURL] Resolved backend URL for production/staging/development. /// [deviceId] Platform-persisted device identifier resolved by the public @@ -563,8 +563,6 @@ class DartBridge { static void _configureLogging(SDKEnvironment environment) { int logLevel; switch (environment) { - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - logLevel = RacLogLevel.info; break; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: logLevel = RacLogLevel.warning; @@ -591,8 +589,6 @@ class DartBridge { /// Swift's `CppBridge.SdkInit.mapEnvironment`. static SdkInitEnvironment _toSdkInitEnvironment(SDKEnvironment env) { switch (env) { - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart index 5d54a6ffb3..42cfca706a 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_environment.dart @@ -107,8 +107,6 @@ class DartBridgeEnvironment { switch (environment) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return RacLogLevel.debug; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return RacLogLevel.info; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return RacLogLevel.warning; default: @@ -165,8 +163,6 @@ class DartBridgeEnvironment { switch (environment) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'Development Environment'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'Staging Environment'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'Production Environment'; default: @@ -321,8 +317,6 @@ class DartBridgeEnvironment { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 0; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 1; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 2; default: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_http.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_http.dart index a28687c2c3..7051d9f956 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_http.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_http.dart @@ -335,8 +335,6 @@ class DartBridgeHTTP { switch (environment) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'https://dev-api.runanywhere.ai'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'https://staging-api.runanywhere.ai'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'https://api.runanywhere.ai'; default: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_state.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_state.dart index 8bebd12b1d..57dfa5df6e 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_state.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_state.dart @@ -303,7 +303,6 @@ class DartBridgeState { case 0: return SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; case 1: - return SDKEnvironment.SDK_ENVIRONMENT_STAGING; case 2: return SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; default: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart index cb454f4109..e9ece47300 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart @@ -431,8 +431,6 @@ class DartBridgeTelemetry { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 0; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 1; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 2; default: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart index 070d655a5e..1e54149a5f 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart @@ -9,15 +9,12 @@ export 'package:runanywhere/generated/model_types.pbenum.dart' show SDKEnvironment; extension SDKEnvironmentExtension on SDKEnvironment { - /// C `rac_environment_t` value (RAC_ENV_DEVELOPMENT/STAGING/PRODUCTION). - /// Mirrors Swift `cEnvironment` (SDKEnvironment.swift:52-59): unknown - /// values map to RAC_ENV_DEVELOPMENT. + /// C `rac_environment_t` value (RAC_ENV_DEVELOPMENT / RAC_ENV_PRODUCTION). + /// Mirrors Swift `cEnvironment`: unknown values map to RAC_ENV_DEVELOPMENT. int get _cEnvironment { switch (this) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 0; // RAC_ENV_DEVELOPMENT - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 1; // RAC_ENV_STAGING case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 2; // RAC_ENV_PRODUCTION default: @@ -37,8 +34,6 @@ extension SDKEnvironmentExtension on SDKEnvironment { switch (this) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'Development Environment'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'Staging Environment'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'Production Environment'; default: @@ -68,8 +63,6 @@ extension SDKEnvironmentExtension on SDKEnvironment { bool get isCompatibleWithCurrentBuild { switch (this) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return true; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: var isDebug = false; assert(() { @@ -95,8 +88,6 @@ extension SDKEnvironmentExtension on SDKEnvironment { switch (this) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return LogLevel.LOG_LEVEL_DEBUG; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return LogLevel.LOG_LEVEL_INFO; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return LogLevel.LOG_LEVEL_WARNING; default: diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index c4df78e962..570bc55c08 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -510,7 +510,6 @@ abstract final class RunAnywhere { // Keyless staging is valid: commons overrides the base URL with the // baked staging backend and requests go out unauthenticated // (PUBLIC-org ingestion). Production stays strict. - final isStaging = environment == SDKEnvironment.SDK_ENVIRONMENT_STAGING; if (!isStaging && (apiKey == null || apiKey.isEmpty)) { throw SDKException.validationFailed( 'API key is required for ${environment.description} mode', @@ -674,7 +673,7 @@ abstract final class RunAnywhere { // whatever the app passed (baked URL, keyless). final effectiveBaseURL = DartBridgeState.instance.baseURL ?? params.baseURL.toString(); - // Effective config from commons state, for every environment: staging + // Effective config from commons state (baked OSS URL fills development when empty) // resolves the baked keyless base URL, dev/prod use whatever the app // passed. There is no direct-to-datastore path — the backend is always // reached through this base URL. Auth stays in C++. Mirrors Swift's unified diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt index 0b9f239afd..bf45a40aef 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeEnvironment.kt @@ -59,8 +59,8 @@ object CppBridgeEnvironment { fun fromC(cEnv: Int): SDKEnvironment = when (cEnv) { 0 -> SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - 1 -> SDKEnvironment.SDK_ENVIRONMENT_STAGING - 2 -> SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION + // Reserved staging slot (1) and production (2) → production. + 1, 2 -> SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION else -> SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeSdkInit.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeSdkInit.kt index f21a49d13f..3930e6d05e 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeSdkInit.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeSdkInit.kt @@ -129,8 +129,6 @@ object CppBridgeSdkInit { private fun SDKEnvironment.toSdkInitEnvironment(): SdkInitEnvironment = when (this) { - SDKEnvironment.SDK_ENVIRONMENT_STAGING -> SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING - SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION -> SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION else -> SdkInitEnvironment.SDK_INIT_ENVIRONMENT_DEVELOPMENT } } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt index 8eeb7c77f5..982be2c657 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt @@ -31,7 +31,7 @@ import okio.ByteString /** * --------------------------------------------------------------------------- * SDK logging configuration. Per-environment presets - * (development/staging/production) stay in each SDK as factory helpers. + * (development/production) stay in each SDK as factory helpers. * --------------------------------------------------------------------------- */ public class LoggingConfiguration( diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt index c66b584f14..5ecc903804 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt @@ -17,15 +17,8 @@ import kotlin.Int import kotlin.Suppress /** - * --------------------------------------------------------------------------- - * SDK environment. Sources pre-IDL: - * Swift SDKEnvironment.swift:5 (development, staging, production) - * Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) - * Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate - * Dart sdk_environment.dart:5 (development, staging, production) - * RN enums.ts:11 (Development, Staging, Production) - * Web enums.ts:9 (Development, Staging, Production) - * --------------------------------------------------------------------------- + * SDK environment — product surface is development + production only. + * never shift PRODUCTION=3. */ public enum class SDKEnvironment( override val `value`: Int, @@ -34,8 +27,6 @@ public enum class SDKEnvironment( SDK_ENVIRONMENT_UNSPECIFIED(0), @RacWireStringOption("development") SDK_ENVIRONMENT_DEVELOPMENT(1), - @RacWireStringOption("staging") - SDK_ENVIRONMENT_STAGING(2), @RacWireStringOption("production") SDK_ENVIRONMENT_PRODUCTION(3), ; @@ -54,8 +45,8 @@ public enum class SDKEnvironment( public fun fromValue(`value`: Int): SDKEnvironment? = when (`value`) { 0 -> SDK_ENVIRONMENT_UNSPECIFIED 1 -> SDK_ENVIRONMENT_DEVELOPMENT - 2 -> SDK_ENVIRONMENT_STAGING - 3 -> SDK_ENVIRONMENT_PRODUCTION + // Former staging (=2) → treat as production at the Kotlin boundary. + 2, 3 -> SDK_ENVIRONMENT_PRODUCTION else -> null } } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt index 54e17ed8ae..9ce71ce037 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt @@ -17,31 +17,12 @@ import kotlin.Int import kotlin.Suppress /** - * --------------------------------------------------------------------------- - * Environment values — must match RAC_ENV_* in - * sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h - * (development=0, staging=1, production=2). Numeric values are part of the - * wire format; do not reorder. - * - * The prior attempt to - * add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke - * Swift iOS at runtime — the shipped librac_commons.a in - * sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the - * original 0/1/2 layout, so Swift sending the regenerated enum value 1 - * (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed - * validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other - * SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped - * layout either, so reverting to the original 0/1/2 wire-format restores - * cross-SDK consistency without requiring a coordinated xcframework rebuild. - * Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of - * every prebuilt commons binary AND regeneration of all five SDK bindings. - * --------------------------------------------------------------------------- + * Environment values — must match RAC_ENV_* (development=0, production=2). */ public enum class SdkInitEnvironment( override val `value`: Int, ) : WireEnum { SDK_INIT_ENVIRONMENT_DEVELOPMENT(0), - SDK_INIT_ENVIRONMENT_STAGING(1), SDK_INIT_ENVIRONMENT_PRODUCTION(2), ; @@ -58,8 +39,8 @@ public enum class SdkInitEnvironment( @JvmStatic public fun fromValue(`value`: Int): SdkInitEnvironment? = when (`value`) { 0 -> SDK_INIT_ENVIRONMENT_DEVELOPMENT - 1 -> SDK_INIT_ENVIRONMENT_STAGING - 2 -> SDK_INIT_ENVIRONMENT_PRODUCTION + // Former staging (=1) → production. + 1, 2 -> SDK_INIT_ENVIRONMENT_PRODUCTION else -> null } } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.kt index e7bc4062fa..2afa8f7e1c 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.kt @@ -104,7 +104,6 @@ public val SDKEnvironment.wireString: String get() = when (this) { SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED -> "unspecified" SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT -> "development" - SDKEnvironment.SDK_ENVIRONMENT_STAGING -> "staging" SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION -> "production" else -> "" } @@ -114,7 +113,6 @@ public fun SDKEnvironment.Companion.fromWireString(value: String): SDKEnvironmen when (value.lowercase()) { "unspecified" -> SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED "development" -> SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT - "staging" -> SDKEnvironment.SDK_ENVIRONMENT_STAGING "production" -> SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION else -> null } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt index e2a8738a68..244cfaaa2c 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt @@ -23,9 +23,9 @@ import kotlinx.coroutines.sync.withLock // epoch-millis timestamp. Proto fields use scalar defaults ("" / 0) instead of // nullable — empty/zero means "unset". -// Per-environment LoggingConfiguration presets. The proto LoggingConfiguration -// is a flat message with no companion helpers, so the development/staging/ -// production factories live here. +// LoggingConfiguration presets. The proto LoggingConfiguration is a flat +// message with no companion helpers, so the named factories live here. +// `staging` is a log-level profile only — not an SDK environment. internal object LoggingConfigurationPresets { /** Development: console + debug level, no device metadata. */ @@ -55,7 +55,6 @@ internal object LoggingConfigurationPresets { fun forEnvironment(environment: SDKEnvironment): LoggingConfiguration = when (environment) { SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT -> development - SDKEnvironment.SDK_ENVIRONMENT_STAGING -> staging SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION -> production SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED -> development } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt index 94c0525965..37c79ff7bb 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt @@ -643,7 +643,7 @@ object RunAnywhereBridge { /** * Register device with backend if not already registered. - * @param environment SDK environment (0=DEVELOPMENT, 1=STAGING, 2=PRODUCTION) + * @param environment SDK environment (0=DEVELOPMENT, 2=PRODUCTION) * @param buildToken Optional build token for development mode */ @JvmStatic @@ -921,7 +921,7 @@ object RunAnywhereBridge { * This must be called during SDK initialization for device registration * to include the correct sdk_version (instead of "unknown"). * - * @param environment Environment (0=development, 1=staging, 2=production) + * @param environment Environment (0=development, 2=production; 1 reserved) * @param deviceId Device ID string * @param platform Platform string (e.g., "android") * @param sdkVersion SDK version string (e.g., "0.1.0") @@ -1454,7 +1454,7 @@ object RunAnywhereBridge { /** Build the JSON body for POST /api/v1/auth/sdk/authenticate. * Returns null on error. The 6-arg signature mirrors rac_sdk_config_t. - * environment: 0 = DEVELOPMENT, 1 = STAGING, 2 = PRODUCTION. */ + * environment: 0 = DEVELOPMENT, 2 = PRODUCTION (1 reserved). */ @JvmStatic external fun racAuthBuildAuthenticateRequest( apiKey: String, baseUrl: String, diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/configuration/SDKEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/configuration/SDKEnvironment.kt index 1a0cc988e2..d7092cbb2d 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/configuration/SDKEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/configuration/SDKEnvironment.kt @@ -23,10 +23,10 @@ import com.runanywhere.sdk.foundation.errors.SDKException /** * SDK environment mode — determines how data is handled. * - * Use the proto enum cases directly: + * Product surface (use these): * - `SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT` - * - `SDKEnvironment.SDK_ENVIRONMENT_STAGING` * - `SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION` + * */ typealias SDKEnvironment = ai.runanywhere.proto.v1.SDKEnvironment @@ -39,15 +39,13 @@ typealias SDKEnvironment = ai.runanywhere.proto.v1.SDKEnvironment // `Companion.fromWireString` factory). /** - * Legacy C-ABI integer (0 = development, 1 = staging, 2 = production). - * Kept so the JNI `rac_environment_t` mapping continues to compile - * unchanged; new code should prefer `toString()` / `wireString` instead. + * Legacy C-ABI integer (0 = development, 2 = production). + * Reserved staging wire value (1) maps to production. */ val SDKEnvironment.cEnvironment: Int get() = when (this) { SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT -> 0 - SDKEnvironment.SDK_ENVIRONMENT_STAGING -> 1 SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION -> 2 SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED -> 0 } @@ -57,7 +55,6 @@ val SDKEnvironment.description: String get() = when (this) { SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT -> "Development Environment" - SDKEnvironment.SDK_ENVIRONMENT_STAGING -> "Staging Environment" SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION -> "Production Environment" SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED -> "Unspecified Environment" } @@ -66,7 +63,6 @@ val deployableSDKEnvironments: List get() = listOf( SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, - SDKEnvironment.SDK_ENVIRONMENT_STAGING, SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION, ) @@ -92,7 +88,6 @@ val SDKEnvironment.defaultLogLevel: LogLevel get() = when (this) { SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT -> LogLevel.LOG_LEVEL_DEBUG - SDKEnvironment.SDK_ENVIRONMENT_STAGING -> LogLevel.LOG_LEVEL_INFO SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION -> LogLevel.LOG_LEVEL_WARNING SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED -> LogLevel.LOG_LEVEL_INFO } @@ -118,10 +113,10 @@ private fun isSDKDebugBuild(): Boolean = * (`sdk/runanywhere-swift/Sources/RunAnywhere/Public/Configuration/SDKEnvironment.swift`): * * - [apiKey] — backend API key for authentication. - * - [baseURL] — backend API base URL. Required for staging/production; a + * - [baseURL] — backend API base URL. Required for production; a * development placeholder is used when the development convenience * constructor is invoked. - * - [environment] — environment mode (development/staging/production). + * - [environment] — environment mode (development/production). * All three Swift convenience initializers are surfaced as Kotlin factories * on the companion object so the call shape mirrors Swift line-for-line. */ @@ -139,9 +134,9 @@ data class SDKInitParams( const val DEVELOPMENT_PLACEHOLDER_URL: String = "https://dev.runanywhere.local" /** - * Create initialization parameters for staging or production. Throws - * [SDKException] when [apiKey] or [baseURL] fail validation against - * the configured [environment]. + * Create initialization parameters for production (or an explicit + * environment). Throws [SDKException] when [apiKey] or [baseURL] fail + * validation against the configured [environment]. * * Mirrors Swift's * `init(apiKey:baseURL:environment:)` (URL-typed) — Kotlin folds the diff --git a/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp b/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp index 887cabc447..9a8ab28684 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp @@ -88,10 +88,17 @@ std::shared_ptr> HybridRunAnywhereCore::initialize( bool discoverDownloadedModels = extractBoolValue(configJson, "discoverDownloadedModels", true); bool rescanLocalModels = extractBoolValue(configJson, "rescanLocalModels", true); - // Determine environment (canonical commons rac_environment_t). + // Product surface: development | production only. rac_environment_t env = RAC_ENV_PRODUCTION; - if (envStr == "development") env = RAC_ENV_DEVELOPMENT; - else if (envStr == "staging") env = RAC_ENV_STAGING; + if (envStr == "development" || envStr == "dev") { + env = RAC_ENV_DEVELOPMENT; + } else if (envStr == "production" || envStr == "prod") { + env = RAC_ENV_PRODUCTION; + } else { + setLastError("Invalid environment '" + envStr + + "' (expected development or production)"); + throw std::invalid_argument("Invalid SDK environment: " + envStr); + } InitBridge::shared().setSdkVersion(sdkVersionFromConfig); diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp index bf1b538a23..ed3d289bbd 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp @@ -2045,7 +2045,7 @@ rac_result_t InitBridge::registerDeviceCallbacks() { ) -> std::tuple { (void)requiresAuth; - // Effective config from commons state, for every environment: staging + // Effective config from commons state (baked OSS URL fills development when empty) // resolves the baked keyless base URL, dev/prod use whatever the app // passed. There is no direct-to-datastore path — the backend is always // reached through this base URL. diff --git a/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp b/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp index e04a5f2029..80857632fa 100644 --- a/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp +++ b/sdk/runanywhere-react-native/packages/core/cpp/bridges/TelemetryBridge.cpp @@ -299,7 +299,7 @@ static void telemetryHttpCallback(void *userData, const char *endpoint, std::string apiKey; { - // Effective config from commons state, for every environment: staging + // Effective config from commons state (baked OSS URL fills development when empty) // overrides whatever the app passed (baked URL, keyless), dev/prod use the // effective URL falling back to the SDK-initialization value. There is no // direct-to-datastore path — the backend is always reached through this URL. diff --git a/sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Models/LoggingConfiguration.ts b/sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Models/LoggingConfiguration.ts index 1a6a855c57..cae7e4b003 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Models/LoggingConfiguration.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Models/LoggingConfiguration.ts @@ -20,6 +20,7 @@ const DEVELOPMENT: LoggingConfiguration = LoggingConfiguration.fromPartial({ minLogLevel: LogLevel.LOG_LEVEL_DEBUG, }); +/** Info-level logging profile — not an SDK environment. */ const STAGING: LoggingConfiguration = LoggingConfiguration.fromPartial({ enableLocalLogging: true, minLogLevel: LogLevel.LOG_LEVEL_INFO, @@ -36,11 +37,14 @@ export function getConfigurationForEnvironment( switch (environment) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return { ...DEVELOPMENT }; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return { ...STAGING }; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return { ...PRODUCTION }; default: return { ...DEVELOPMENT }; } } + +/** Info-level logging profile — not tied to an SDK environment. */ +export function getStagingConfiguration(): LoggingConfiguration { + return { ...STAGING }; +} diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKEnvironment+Helpers.ts b/sdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKEnvironment+Helpers.ts index c03007d56c..e8e8df02a3 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKEnvironment+Helpers.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKEnvironment+Helpers.ts @@ -2,40 +2,22 @@ * SDKEnvironment+Helpers.ts * * Behaviour helpers for the proto-generated `SDKEnvironment` enum. - * - * Mirrors Swift `SDKEnvironment.swift:42-128`. Swift delegates to the - * commons C ABI (`rac_env_is_production`, `rac_env_should_send_telemetry`, - * …); the C string/predicate table is not exposed through the Nitro proto - * bridge, so the exact same logic is mirrored here from - * `sdk/runanywhere-commons/src/infrastructure/network/environment.cpp`. - * TODO(layer-down): expose the rac_env_* predicates through the Nitro - * bridge and delegate, removing this mirrored table. - * - * Swift's `isCompatibleWithCurrentBuild` / `isDebugBuild` are not ported: - * they depend on the `#if DEBUG` compile-time flag, which has no - * cross-bundle RN equivalent. + * Mirrors Swift `SDKEnvironment.swift` / commons `rac_env_*` predicates. */ import { SDKEnvironment } from '@runanywhere/proto-ts/model_types'; import { LogLevel } from '@runanywhere/proto-ts/logging'; -/** - * All three deployable environments, excluding UNSPECIFIED/UNRECOGNIZED. - * Mirrors Swift `SDKEnvironment.deployableCases`. - */ +/** Deployable product environments (development + production). */ export const deployableEnvironments: readonly SDKEnvironment[] = [ SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, - SDKEnvironment.SDK_ENVIRONMENT_STAGING, SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION, ]; -/** Human-readable description. Mirrors Swift `SDKEnvironment.description`. */ export function environmentDescription(env: SDKEnvironment): string { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'Development Environment'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'Staging Environment'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'Production Environment'; default: @@ -43,69 +25,34 @@ export function environmentDescription(env: SDKEnvironment): string { } } -/** - * Whether this is a production environment. - * Mirrors Swift `isProduction` → `rac_env_is_production` (env == PRODUCTION). - */ export function isProduction(env: SDKEnvironment): boolean { return env === SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; } -/** - * Whether this is a testing environment. - * Mirrors Swift `isTesting` → `rac_env_is_testing` - * (env == DEVELOPMENT || env == STAGING). - */ export function isTesting(env: SDKEnvironment): boolean { - return ( - env === SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT || - env === SDKEnvironment.SDK_ENVIRONMENT_STAGING - ); + return env === SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Whether this environment requires a valid backend URL. - * Mirrors Swift `requiresBackendURL` → `rac_env_requires_backend_url` - * (env != DEVELOPMENT). - */ export function requiresBackendURL(env: SDKEnvironment): boolean { return env !== SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Whether telemetry should be sent (production only). - * Mirrors Swift `shouldSendTelemetry` → `rac_env_should_send_telemetry`. - */ -export function shouldSendTelemetry(env: SDKEnvironment): boolean { - return env === SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; +export function shouldSendTelemetry(_env: SDKEnvironment): boolean { + return true; } -/** - * Whether to sync with the backend (non-development). - * Mirrors Swift `shouldSyncWithBackend` → `rac_env_should_sync_with_backend`. - */ export function shouldSyncWithBackend(env: SDKEnvironment): boolean { return env !== SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Whether API authentication is required (non-development). - * Mirrors Swift `requiresAuthentication` → `rac_env_requires_auth`. - */ export function requiresAuthentication(env: SDKEnvironment): boolean { return env !== SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Default logging verbosity for an environment. - * Mirrors Swift `defaultLogLevel` (SDKEnvironment.swift:112-119). - */ export function defaultLogLevel(env: SDKEnvironment): LogLevel { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return LogLevel.LOG_LEVEL_DEBUG; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return LogLevel.LOG_LEVEL_INFO; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return LogLevel.LOG_LEVEL_WARNING; default: diff --git a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts index 305b9e28eb..14b041a517 100644 --- a/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts @@ -152,8 +152,6 @@ function mapSdkInitEnvironment( environment: SDKEnvironment ): SdkInitEnvironment { switch (environment) { - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: @@ -165,8 +163,6 @@ function mapSdkInitEnvironment( function environmentToConfigString(environment: SDKEnvironment): string { switch (environment) { - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'staging'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'production'; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: diff --git a/sdk/runanywhere-react-native/packages/core/src/types/models.ts b/sdk/runanywhere-react-native/packages/core/src/types/models.ts index 678c67adb5..de69fe10b1 100644 --- a/sdk/runanywhere-react-native/packages/core/src/types/models.ts +++ b/sdk/runanywhere-react-native/packages/core/src/types/models.ts @@ -19,7 +19,7 @@ export interface SDKInitOptions { /** API key for authentication (production/staging) */ apiKey?: string; - /** Base URL for API requests (production: Railway endpoint) */ + /** Base URL for API requests */ baseURL?: string; /** SDK environment */ diff --git a/sdk/runanywhere-swift/ARCHITECTURE.md b/sdk/runanywhere-swift/ARCHITECTURE.md index 1e37b0054d..9911e1bb72 100644 --- a/sdk/runanywhere-swift/ARCHITECTURE.md +++ b/sdk/runanywhere-swift/ARCHITECTURE.md @@ -211,7 +211,7 @@ public typealias SDKEnvironment = RASDKEnvironment // line 20 `RASDKEnvironment` is the proto3-generated enum from `idl/model_types.proto`. The hand-written enum was removed; all SDK logic operates on the generated type through extensions. -Cases (from proto): `.development`, `.staging`, `.production`, `.unspecified` (proto default), `UNRECOGNIZED` (proto catch-all). +Cases (from proto): `.development`, `.production`, `.unspecified` (proto default), `UNRECOGNIZED` (proto catch-all). Staging was removed; its wire number is reserved. #### Codable conformance (lines 29–39) @@ -221,15 +221,15 @@ Cases (from proto): `.development`, `.staging`, `.production`, `.unspecified` (p | Symbol | Type | Mechanism | Line | |---|---|---|---| -| `deployableCases` | `static [RASDKEnvironment]` | Hardcoded array `[.development, .staging, .production]` | 47 | -| `cEnvironment` | `rac_environment_t` | Switch to C enum constants (`RAC_ENV_DEVELOPMENT`, `RAC_ENV_STAGING`, `RAC_ENV_PRODUCTION`) | 54 | +| `deployableCases` | `static [RASDKEnvironment]` | Hardcoded array `[.development, .production]` | 47 | +| `cEnvironment` | `rac_environment_t` | Switch to C enum constants (`RAC_ENV_DEVELOPMENT`, `RAC_ENV_PRODUCTION`) | 54 | | `description` | `String` | Switch returning human-readable label | 64 | | `isProduction` | `Bool` | `rac_env_is_production(cEnvironment)` | 74 | | `isTesting` | `Bool` | `rac_env_is_testing(cEnvironment)` | 77 | | `requiresBackendURL` | `Bool` | `rac_env_requires_backend_url(cEnvironment)` | 80 | | `isCompatibleWithCurrentBuild` | `Bool` | Swift `#if DEBUG` check; production returns `false` in DEBUG builds | 86 | | `isDebugBuild` | `static Bool` | `#if DEBUG` flag | 102 | -| `defaultLogLevel` | `LogLevel` | Switch: `.development` → `.debug`, `.staging` → `.info`, `.production` → `.warning` | 113 | +| `defaultLogLevel` | `LogLevel` | Switch: `.development` → `.debug`, `.production` → `.warning` | 113 | | `shouldSendTelemetry` | `Bool` | `rac_env_should_send_telemetry(cEnvironment)` | 123 | | `useMockData` | `Bool` | `self == .development` | 126 | | `shouldSyncWithBackend` | `Bool` | `rac_env_should_sync_with_backend(cEnvironment)` | 129 | @@ -1840,7 +1840,7 @@ Metadata sanitization: `sanitizeMetadata(_:)` calls `rac_log_metadata_should_red Pre-instantiated: `SDKLogger.shared` ("RunAnywhere"), `.llm`, `.stt`, `.tts`, `.download`, `.models`. -Environment presets: `.development` → minLevel `.debug`; `.staging` → `.info`; `.production` → `.warning`. +Environment presets: `.development` → minLevel `.debug`; `.production` → `.warning`. #### §9.4.2 Custom Log Destinations diff --git a/sdk/runanywhere-swift/README.md b/sdk/runanywhere-swift/README.md index 5823329e1c..3f458bc2eb 100644 --- a/sdk/runanywhere-swift/README.md +++ b/sdk/runanywhere-swift/README.md @@ -248,7 +248,6 @@ try RunAnywhere.initialize( | Environment | Description | |-----------------|--------------------------------------------------| | `.development` | Verbose logging, mock services, local analytics | -| `.staging` | Testing with real services | | `.production` | Minimal logging, full authentication, telemetry | ### Generation Options diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h b/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h index 2f4b744e98..a101359fd7 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_dev_config.h @@ -31,9 +31,9 @@ extern "C" { /** * @brief Get the baked staging backend base URL * - * Team builds bake the staging URL via the git-ignored development_config.cpp - * so callers can init with environment=staging and nothing else. Open-source - * builds keep the placeholder and must pass a base URL explicitly. + * Release/CI bakes the OSS backend URL (STAGING_BASE_URL secret) so keyless + * development can omit base_url. Open-source placeholder builds must pass a + * base URL explicitly (or bake STAGING_BASE_URL at compile time). * * @return URL string or placeholder (static, do not free) */ diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift index 5a429e7107..0a8319d4b5 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Environment.swift @@ -20,7 +20,6 @@ extension CppBridge { public static func toC(_ env: SDKEnvironment) -> rac_environment_t { switch env { case .development: return RAC_ENV_DEVELOPMENT - case .staging: return RAC_ENV_STAGING case .production: return RAC_ENV_PRODUCTION default: return RAC_ENV_DEVELOPMENT } @@ -30,7 +29,6 @@ extension CppBridge { public static func fromC(_ env: rac_environment_t) -> SDKEnvironment { switch env { case RAC_ENV_DEVELOPMENT: return .development - case RAC_ENV_STAGING: return .staging case RAC_ENV_PRODUCTION: return .production default: return .development } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+SdkInit.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+SdkInit.swift index 2a7a156635..20e29ba949 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+SdkInit.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+SdkInit.swift @@ -123,7 +123,6 @@ extension CppBridge { private static func mapEnvironment(_ env: SDKEnvironment) -> RASdkInitEnvironment { switch env { case .development: return .development - case .staging: return .staging case .production: return .production default: return .development } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swift index da16b5b6d6..b46a46e581 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swift @@ -100,7 +100,6 @@ extension RASDKEnvironment { switch self { case .unspecified: return "unspecified" case .development: return "development" - case .staging: return "staging" case .production: return "production" default: return "" } @@ -114,7 +113,6 @@ extension RASDKEnvironment { switch wireString.lowercased() { case "unspecified": return .unspecified case "development": return .development - case "staging": return .staging case "production": return .production default: return nil } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/model_types.pb.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/model_types.pb.swift index 791d754210..cf95ae3f97 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/model_types.pb.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/model_types.pb.swift @@ -423,19 +423,14 @@ public nonisolated enum RAModelCategory: SwiftProtobuf.Enum, Swift.CaseIterable } /// --------------------------------------------------------------------------- -/// SDK environment. Sources pre-IDL: -/// Swift SDKEnvironment.swift:5 (development, staging, production) -/// Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) -/// Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate -/// Dart sdk_environment.dart:5 (development, staging, production) -/// RN enums.ts:11 (Development, Staging, Production) -/// Web enums.ts:9 (Development, Staging, Production) +/// SDK environment — product surface is development + production only. +/// Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values +/// never shift PRODUCTION=3. /// --------------------------------------------------------------------------- public nonisolated enum RASDKEnvironment: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case unspecified // = 0 case development // = 1 - case staging // = 2 case production // = 3 case UNRECOGNIZED(Int) @@ -447,7 +442,6 @@ public nonisolated enum RASDKEnvironment: SwiftProtobuf.Enum, Swift.CaseIterable switch rawValue { case 0: self = .unspecified case 1: self = .development - case 2: self = .staging case 3: self = .production default: self = .UNRECOGNIZED(rawValue) } @@ -457,7 +451,6 @@ public nonisolated enum RASDKEnvironment: SwiftProtobuf.Enum, Swift.CaseIterable switch self { case .unspecified: return 0 case .development: return 1 - case .staging: return 2 case .production: return 3 case .UNRECOGNIZED(let i): return i } @@ -467,7 +460,6 @@ public nonisolated enum RASDKEnvironment: SwiftProtobuf.Enum, Swift.CaseIterable public static let allCases: [RASDKEnvironment] = [ .unspecified, .development, - .staging, .production, ] @@ -2911,7 +2903,7 @@ nonisolated extension RAModelCategory: SwiftProtobuf._ProtoNameProviding { } nonisolated extension RASDKEnvironment: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0SDK_ENVIRONMENT_UNSPECIFIED\0\u{1}SDK_ENVIRONMENT_DEVELOPMENT\0\u{1}SDK_ENVIRONMENT_STAGING\0\u{1}SDK_ENVIRONMENT_PRODUCTION\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0SDK_ENVIRONMENT_UNSPECIFIED\0\u{1}SDK_ENVIRONMENT_DEVELOPMENT\0\u{2}\u{2}SDK_ENVIRONMENT_PRODUCTION\0") } nonisolated extension RAModelSource: SwiftProtobuf._ProtoNameProviding { diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_init.pb.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_init.pb.swift index 66f23ccdbd..230344c839 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_init.pb.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Generated/sdk_init.pb.swift @@ -100,26 +100,13 @@ public nonisolated enum RASdkInitPhase: SwiftProtobuf.Enum, Swift.CaseIterable { /// --------------------------------------------------------------------------- /// Environment values — must match RAC_ENV_* in /// sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h -/// (development=0, staging=1, production=2). Numeric values are part of the -/// wire format; do not reorder. -/// -/// The prior attempt to -/// add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke -/// Swift iOS at runtime — the shipped librac_commons.a in -/// sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the -/// original 0/1/2 layout, so Swift sending the regenerated enum value 1 -/// (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed -/// validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other -/// SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped -/// layout either, so reverting to the original 0/1/2 wire-format restores -/// cross-SDK consistency without requiring a coordinated xcframework rebuild. -/// Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of -/// every prebuilt commons binary AND regeneration of all five SDK bindings. +/// (development=0, production=2). Numeric values are part of the wire format; +/// do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is +/// reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). /// --------------------------------------------------------------------------- public nonisolated enum RASdkInitEnvironment: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case development // = 0 - case staging // = 1 case production // = 2 case UNRECOGNIZED(Int) @@ -130,7 +117,6 @@ public nonisolated enum RASdkInitEnvironment: SwiftProtobuf.Enum, Swift.CaseIter public init?(rawValue: Int) { switch rawValue { case 0: self = .development - case 1: self = .staging case 2: self = .production default: self = .UNRECOGNIZED(rawValue) } @@ -139,7 +125,6 @@ public nonisolated enum RASdkInitEnvironment: SwiftProtobuf.Enum, Swift.CaseIter public var rawValue: Int { switch self { case .development: return 0 - case .staging: return 1 case .production: return 2 case .UNRECOGNIZED(let i): return i } @@ -148,7 +133,6 @@ public nonisolated enum RASdkInitEnvironment: SwiftProtobuf.Enum, Swift.CaseIter // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RASdkInitEnvironment] = [ .development, - .staging, .production, ] @@ -337,7 +321,7 @@ nonisolated extension RASdkInitPhase: SwiftProtobuf._ProtoNameProviding { } nonisolated extension RASdkInitEnvironment: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0SDK_INIT_ENVIRONMENT_DEVELOPMENT\0\u{1}SDK_INIT_ENVIRONMENT_STAGING\0\u{1}SDK_INIT_ENVIRONMENT_PRODUCTION\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0SDK_INIT_ENVIRONMENT_DEVELOPMENT\0\u{2}\u{2}SDK_INIT_ENVIRONMENT_PRODUCTION\0") } nonisolated extension RASdkInitPhase1Request: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Logging/SDKLogger.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Logging/SDKLogger.swift index 633652e5cc..55e1cc2131 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Logging/SDKLogger.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Logging/SDKLogger.swift @@ -34,9 +34,10 @@ public protocol LogDestination: AnyObject, Sendable { // swiftlint:disable:this // MARK: - RALoggingConfiguration Presets -/// Environment presets for the generated `RALoggingConfiguration` proto. +/// Logging presets for the generated `RALoggingConfiguration` proto. /// The struct itself is generated under `Sources/RunAnywhere/Generated/`; -/// these factories supply the dev/staging/prod defaults. +/// these factories supply named defaults. `.staging` is a log-level profile +/// only — not an SDK environment. extension RALoggingConfiguration { public static var development: RALoggingConfiguration { var config = RALoggingConfiguration() @@ -255,7 +256,6 @@ extension RALoggingConfiguration { static func forEnvironment(_ environment: SDKEnvironment) -> RALoggingConfiguration { switch environment { case .development: return .development - case .staging: return .staging case .production: return .production default: return .development } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Configuration/SDKEnvironment.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Configuration/SDKEnvironment.swift index d6364f85ce..ae6831c27e 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Configuration/SDKEnvironment.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/Configuration/SDKEnvironment.swift @@ -40,11 +40,9 @@ extension RASDKEnvironment: Codable { // MARK: - Extensions (preserved from the hand-written enum) public extension RASDKEnvironment { - /// All three deployable environments, excluding `.unspecified` / - /// `UNRECOGNIZED`. Preserves the `CaseIterable.allCases` semantics of - /// the pre-IDL hand-written enum. + /// Deployable product environments (development + production). static var deployableCases: [RASDKEnvironment] { - [.development, .staging, .production] + [.development, .production] } // MARK: - C++ Bridge @@ -53,7 +51,6 @@ public extension RASDKEnvironment { var cEnvironment: rac_environment_t { switch self { case .development: return RAC_ENV_DEVELOPMENT - case .staging: return RAC_ENV_STAGING case .production: return RAC_ENV_PRODUCTION default: return RAC_ENV_DEVELOPMENT } @@ -63,7 +60,6 @@ public extension RASDKEnvironment { var description: String { switch self { case .development: return "Development Environment" - case .staging: return "Staging Environment" case .production: return "Production Environment" default: return "Unspecified Environment" } @@ -84,7 +80,7 @@ public extension RASDKEnvironment { /// environment. Production is only allowed in Release builds. var isCompatibleWithCurrentBuild: Bool { switch self { - case .development, .staging: + case .development: return true case .production: #if DEBUG @@ -112,7 +108,6 @@ public extension RASDKEnvironment { var defaultLogLevel: RALogLevel { switch self { case .development: return .debug - case .staging: return .info case .production: return .warning default: return .info } @@ -133,11 +128,11 @@ public struct SDKInitParams: Sendable { /// API key for authentication. public let apiKey: String - /// Base URL for API requests. Required for staging/production; optional + /// Base URL for API requests. Required for production; optional /// for development (uses placeholder if not provided). public let baseURL: URL - /// Environment mode (development/staging/production). + /// Environment mode (development/production). public let environment: SDKEnvironment // MARK: - Default Development URL @@ -153,7 +148,7 @@ public struct SDKInitParams: Sendable { // MARK: - Initializers - /// Create initialization parameters for staging or production. + /// Create initialization parameters for production. public init( apiKey: String, baseURL: URL, @@ -166,7 +161,7 @@ public struct SDKInitParams: Sendable { try Self.validate(apiKey: apiKey, baseURL: baseURL, environment: environment) } - /// Convenience initializer with string URL for staging or production. + /// Convenience initializer with string URL for production. public init( apiKey: String, baseURL: String, @@ -219,16 +214,7 @@ public struct SDKInitParams: Sendable { throw SDKException(code: .validationFailed, message: message, category: .internal) } - if environment == .staging, baseURL.scheme?.lowercased() == "http" { - logger.warning("Using HTTP for staging environment. Consider using HTTPS for security.") - } - if environment == .staging, let host = baseURL.host?.lowercased() { - if host.contains("localhost") || host.contains("127.0.0.1") || - host.contains("example.com") || host.contains(".local") { - logger.warning("Staging environment using local/example URL: \(host)") - } - } logger.info("URL validated for \(environment.description): \(baseURL.absoluteString)") } diff --git a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift index 2ec17928b4..01f79c5c93 100644 --- a/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift +++ b/sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift @@ -412,7 +412,7 @@ public enum RunAnywhere { // Step 1: configure the Swift HTTP adapter used by callback-based // platform services. Auth and control-plane orchestration stay in C++. if await !CppBridge.HTTP.shared.isConfigured { - // Effective config from commons state, for every environment: staging + // Effective config from commons state (baked OSS URL fills development when empty) // resolves the baked keyless base URL, dev/prod use whatever the app // passed. There is no direct-to-datastore path — the backend is always // reached through this base URL. Auth stays in C++. diff --git a/sdk/runanywhere-web/packages/core/src/Foundation/SDKEnvironment+Helpers.ts b/sdk/runanywhere-web/packages/core/src/Foundation/SDKEnvironment+Helpers.ts index 153382ca2d..92eba6303d 100644 --- a/sdk/runanywhere-web/packages/core/src/Foundation/SDKEnvironment+Helpers.ts +++ b/sdk/runanywhere-web/packages/core/src/Foundation/SDKEnvironment+Helpers.ts @@ -3,45 +3,28 @@ * * Standalone helper functions over the proto-generated `SDKEnvironment` * enum (idl/model_types.proto). Port of the Swift extension members on - * `RASDKEnvironment` (SDKEnvironment.swift:42-128), which delegate to the - * C commons env predicates (rac_environment.h / environment.cpp). Web - * cannot call those C helpers synchronously before WASM is loaded, so the - * (trivial, stable) predicate logic is mirrored here 1:1 from - * `sdk/runanywhere-commons/src/infrastructure/network/environment.cpp`. - * - * Wire-format helpers are NOT re-implemented here: use the codegen-generated - * `sDKEnvironmentWireString` / `sDKEnvironmentFromWireString` from - * `@runanywhere/proto-ts/convenience/model_types_convenience` (they back - * Swift's Codable conformance, SDKEnvironment.swift:28-38). + * `RASDKEnvironment` (SDKEnvironment.swift), which delegate to the + * C commons env predicates (rac_environment.h / environment.cpp). */ import { LogLevel } from '@runanywhere/proto-ts/logging'; import { SDKEnvironment } from '@runanywhere/proto-ts/model_types'; /** - * All three deployable environments, excluding UNSPECIFIED / UNRECOGNIZED. - * Swift parity: `RASDKEnvironment.deployableCases` (SDKEnvironment.swift:46-48). + * Deployable product environments (development + production). */ export function environmentDeployableCases(): SDKEnvironment[] { return [ SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT, - SDKEnvironment.SDK_ENVIRONMENT_STAGING, SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION, ]; } /** - * Normalize to one of the three deployable environments. - * - * Mirrors Swift's `cEnvironment` bridge (SDKEnvironment.swift:53-60), whose - * `default:` arm maps UNSPECIFIED / UNRECOGNIZED to development before the C - * predicates run. All boolean helpers below go through this so they return - * exactly what Swift's C-backed computed properties return. + * Normalize to a product environment (UNSPECIFIED → development). */ function normalizedEnvironment(env: SDKEnvironment): SDKEnvironment { switch (env) { - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return SDKEnvironment.SDK_ENVIRONMENT_STAGING; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; default: @@ -49,16 +32,10 @@ function normalizedEnvironment(env: SDKEnvironment): SDKEnvironment { } } -/** - * Human-readable description. - * Swift parity: `RASDKEnvironment.description` (SDKEnvironment.swift:63-70). - */ export function environmentDescription(env: SDKEnvironment): string { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'Development Environment'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'Staging Environment'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'Production Environment'; default: @@ -66,49 +43,22 @@ export function environmentDescription(env: SDKEnvironment): string { } } -/** - * Check if this is a production environment. - * Swift parity: `RASDKEnvironment.isProduction` (SDKEnvironment.swift:73), - * backed by `rac_env_is_production` (environment.cpp:39-41). - */ export function environmentIsProduction(env: SDKEnvironment): boolean { return normalizedEnvironment(env) === SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; } -/** - * Check if this is a testing environment (development or staging). - * Swift parity: `RASDKEnvironment.isTesting` (SDKEnvironment.swift:76), - * backed by `rac_env_is_testing` (environment.cpp:43-45). - */ export function environmentIsTesting(env: SDKEnvironment): boolean { - const normalized = normalizedEnvironment(env); - return ( - normalized === SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT || - normalized === SDKEnvironment.SDK_ENVIRONMENT_STAGING - ); + return normalizedEnvironment(env) === SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Check if this environment requires a valid backend URL (non-development). - * Swift parity: `RASDKEnvironment.requiresBackendURL` (SDKEnvironment.swift:79), - * backed by `rac_env_requires_backend_url` (environment.cpp:35-37). - */ export function environmentRequiresBackendURL(env: SDKEnvironment): boolean { return normalizedEnvironment(env) !== SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Determine logging verbosity based on environment. - * Swift parity: `RASDKEnvironment.defaultLogLevel` (SDKEnvironment.swift:112-119). - * Note: Swift switches on the proto value directly (default → .info) without - * the `cEnvironment` normalization, so UNSPECIFIED yields INFO here too. - */ export function environmentDefaultLogLevel(env: SDKEnvironment): LogLevel { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return LogLevel.LOG_LEVEL_DEBUG; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return LogLevel.LOG_LEVEL_INFO; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return LogLevel.LOG_LEVEL_WARNING; default: @@ -116,29 +66,14 @@ export function environmentDefaultLogLevel(env: SDKEnvironment): LogLevel { } } -/** - * Should send telemetry data (production only). - * Swift parity: `RASDKEnvironment.shouldSendTelemetry` (SDKEnvironment.swift:122), - * backed by `rac_env_should_send_telemetry` (environment.cpp:60-62). - */ -export function environmentShouldSendTelemetry(env: SDKEnvironment): boolean { - return normalizedEnvironment(env) === SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; +export function environmentShouldSendTelemetry(_env: SDKEnvironment): boolean { + return true; } -/** - * Should sync with backend (non-development). - * Swift parity: `RASDKEnvironment.shouldSyncWithBackend` (SDKEnvironment.swift:125), - * backed by `rac_env_should_sync_with_backend` (environment.cpp:64-66). - */ export function environmentShouldSyncWithBackend(env: SDKEnvironment): boolean { return normalizedEnvironment(env) !== SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } -/** - * Requires API authentication (non-development). - * Swift parity: `RASDKEnvironment.requiresAuthentication` (SDKEnvironment.swift:128), - * backed by `rac_env_requires_auth` (environment.cpp:31-33). - */ export function environmentRequiresAuthentication(env: SDKEnvironment): boolean { return normalizedEnvironment(env) !== SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; } diff --git a/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts b/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts index 06179a30d1..8ce81bf87e 100644 --- a/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts +++ b/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts @@ -26,15 +26,13 @@ export type { LoggingConfiguration }; /** * Environment presets — Swift parity: `RALoggingConfiguration.development / - * .staging / .production` (SDKLogger.swift:41-66). Dev logs at debug with + * .production` (SDKLogger.swift:41-66). Dev logs at debug with * local logging on; production logs warnings only with local logging off. */ export function loggingConfigurationForEnvironment( environment: SDKEnvironment, ): LoggingConfiguration { switch (environment) { - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return LoggingConfigurationProto.fromPartial({ enableLocalLogging: true, minLogLevel: LogLevel.LOG_LEVEL_INFO, includeSourceLocation: false, diff --git a/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts b/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts index 437a9e0a83..d9e92fbd6f 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/RunAnywhere.ts @@ -263,8 +263,6 @@ function mapSdkInitEnvironment(env: SDKEnvironment): SdkInitEnvironment { switch (env) { case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: default: return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_DEVELOPMENT; diff --git a/sdk/shared/proto-ts/dist/convenience/model_types_convenience.js b/sdk/shared/proto-ts/dist/convenience/model_types_convenience.js index ad0791c627..5251f1d269 100644 --- a/sdk/shared/proto-ts/dist/convenience/model_types_convenience.js +++ b/sdk/shared/proto-ts/dist/convenience/model_types_convenience.js @@ -130,8 +130,6 @@ const sDKEnvironmentWireString = (e) => { return 'unspecified'; case model_types_1.SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'development'; - case model_types_1.SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'staging'; case model_types_1.SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'production'; default: @@ -145,8 +143,6 @@ const sDKEnvironmentFromWireString = (s) => { return model_types_1.SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED; case 'development': return model_types_1.SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - case 'staging': - return model_types_1.SDKEnvironment.SDK_ENVIRONMENT_STAGING; case 'production': return model_types_1.SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; default: diff --git a/sdk/shared/proto-ts/dist/model_types.d.ts b/sdk/shared/proto-ts/dist/model_types.d.ts index c91953e7d6..e311d0f506 100644 --- a/sdk/shared/proto-ts/dist/model_types.d.ts +++ b/sdk/shared/proto-ts/dist/model_types.d.ts @@ -140,19 +140,14 @@ export declare function modelCategoryFromJSON(object: any): ModelCategory; export declare function modelCategoryToJSON(object: ModelCategory): string; /** * --------------------------------------------------------------------------- - * SDK environment. Sources pre-IDL: - * Swift SDKEnvironment.swift:5 (development, staging, production) - * Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) - * Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate - * Dart sdk_environment.dart:5 (development, staging, production) - * RN enums.ts:11 (Development, Staging, Production) - * Web enums.ts:9 (Development, Staging, Production) + * SDK environment — product surface is development + production only. + * Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values + * never shift PRODUCTION=3. * --------------------------------------------------------------------------- */ export declare enum SDKEnvironment { SDK_ENVIRONMENT_UNSPECIFIED = 0, SDK_ENVIRONMENT_DEVELOPMENT = 1, - SDK_ENVIRONMENT_STAGING = 2, SDK_ENVIRONMENT_PRODUCTION = 3, UNRECOGNIZED = -1 } diff --git a/sdk/shared/proto-ts/dist/model_types.js b/sdk/shared/proto-ts/dist/model_types.js index 9725657005..5362fbf6c6 100644 --- a/sdk/shared/proto-ts/dist/model_types.js +++ b/sdk/shared/proto-ts/dist/model_types.js @@ -514,20 +514,15 @@ function modelCategoryToJSON(object) { } /** * --------------------------------------------------------------------------- - * SDK environment. Sources pre-IDL: - * Swift SDKEnvironment.swift:5 (development, staging, production) - * Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) - * Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate - * Dart sdk_environment.dart:5 (development, staging, production) - * RN enums.ts:11 (Development, Staging, Production) - * Web enums.ts:9 (Development, Staging, Production) + * SDK environment — product surface is development + production only. + * Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values + * never shift PRODUCTION=3. * --------------------------------------------------------------------------- */ var SDKEnvironment; (function (SDKEnvironment) { SDKEnvironment[SDKEnvironment["SDK_ENVIRONMENT_UNSPECIFIED"] = 0] = "SDK_ENVIRONMENT_UNSPECIFIED"; SDKEnvironment[SDKEnvironment["SDK_ENVIRONMENT_DEVELOPMENT"] = 1] = "SDK_ENVIRONMENT_DEVELOPMENT"; - SDKEnvironment[SDKEnvironment["SDK_ENVIRONMENT_STAGING"] = 2] = "SDK_ENVIRONMENT_STAGING"; SDKEnvironment[SDKEnvironment["SDK_ENVIRONMENT_PRODUCTION"] = 3] = "SDK_ENVIRONMENT_PRODUCTION"; SDKEnvironment[SDKEnvironment["UNRECOGNIZED"] = -1] = "UNRECOGNIZED"; })(SDKEnvironment || (exports.SDKEnvironment = SDKEnvironment = {})); @@ -539,9 +534,6 @@ function sDKEnvironmentFromJSON(object) { case 1: case "SDK_ENVIRONMENT_DEVELOPMENT": return SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - case 2: - case "SDK_ENVIRONMENT_STAGING": - return SDKEnvironment.SDK_ENVIRONMENT_STAGING; case 3: case "SDK_ENVIRONMENT_PRODUCTION": return SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; @@ -557,8 +549,6 @@ function sDKEnvironmentToJSON(object) { return "SDK_ENVIRONMENT_UNSPECIFIED"; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return "SDK_ENVIRONMENT_DEVELOPMENT"; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return "SDK_ENVIRONMENT_STAGING"; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return "SDK_ENVIRONMENT_PRODUCTION"; case SDKEnvironment.UNRECOGNIZED: diff --git a/sdk/shared/proto-ts/dist/sdk_init.d.ts b/sdk/shared/proto-ts/dist/sdk_init.d.ts index 28c2556108..479bac0dff 100644 --- a/sdk/shared/proto-ts/dist/sdk_init.d.ts +++ b/sdk/shared/proto-ts/dist/sdk_init.d.ts @@ -24,26 +24,13 @@ export declare function sdkInitPhaseToJSON(object: SdkInitPhase): string; * --------------------------------------------------------------------------- * Environment values — must match RAC_ENV_* in * sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h - * (development=0, staging=1, production=2). Numeric values are part of the - * wire format; do not reorder. - * - * The prior attempt to - * add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke - * Swift iOS at runtime — the shipped librac_commons.a in - * sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the - * original 0/1/2 layout, so Swift sending the regenerated enum value 1 - * (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed - * validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other - * SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped - * layout either, so reverting to the original 0/1/2 wire-format restores - * cross-SDK consistency without requiring a coordinated xcframework rebuild. - * Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of - * every prebuilt commons binary AND regeneration of all five SDK bindings. + * (development=0, production=2). Numeric values are part of the wire format; + * do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is + * reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). * --------------------------------------------------------------------------- */ export declare enum SdkInitEnvironment { SDK_INIT_ENVIRONMENT_DEVELOPMENT = 0, - SDK_INIT_ENVIRONMENT_STAGING = 1, SDK_INIT_ENVIRONMENT_PRODUCTION = 2, UNRECOGNIZED = -1 } diff --git a/sdk/shared/proto-ts/dist/sdk_init.js b/sdk/shared/proto-ts/dist/sdk_init.js index 124dfd3ee4..c8a8f7958d 100644 --- a/sdk/shared/proto-ts/dist/sdk_init.js +++ b/sdk/shared/proto-ts/dist/sdk_init.js @@ -71,27 +71,14 @@ function sdkInitPhaseToJSON(object) { * --------------------------------------------------------------------------- * Environment values — must match RAC_ENV_* in * sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h - * (development=0, staging=1, production=2). Numeric values are part of the - * wire format; do not reorder. - * - * The prior attempt to - * add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke - * Swift iOS at runtime — the shipped librac_commons.a in - * sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the - * original 0/1/2 layout, so Swift sending the regenerated enum value 1 - * (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed - * validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other - * SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped - * layout either, so reverting to the original 0/1/2 wire-format restores - * cross-SDK consistency without requiring a coordinated xcframework rebuild. - * Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of - * every prebuilt commons binary AND regeneration of all five SDK bindings. + * (development=0, production=2). Numeric values are part of the wire format; + * do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is + * reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). * --------------------------------------------------------------------------- */ var SdkInitEnvironment; (function (SdkInitEnvironment) { SdkInitEnvironment[SdkInitEnvironment["SDK_INIT_ENVIRONMENT_DEVELOPMENT"] = 0] = "SDK_INIT_ENVIRONMENT_DEVELOPMENT"; - SdkInitEnvironment[SdkInitEnvironment["SDK_INIT_ENVIRONMENT_STAGING"] = 1] = "SDK_INIT_ENVIRONMENT_STAGING"; SdkInitEnvironment[SdkInitEnvironment["SDK_INIT_ENVIRONMENT_PRODUCTION"] = 2] = "SDK_INIT_ENVIRONMENT_PRODUCTION"; SdkInitEnvironment[SdkInitEnvironment["UNRECOGNIZED"] = -1] = "UNRECOGNIZED"; })(SdkInitEnvironment || (exports.SdkInitEnvironment = SdkInitEnvironment = {})); @@ -100,9 +87,6 @@ function sdkInitEnvironmentFromJSON(object) { case 0: case "SDK_INIT_ENVIRONMENT_DEVELOPMENT": return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_DEVELOPMENT; - case 1: - case "SDK_INIT_ENVIRONMENT_STAGING": - return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING; case 2: case "SDK_INIT_ENVIRONMENT_PRODUCTION": return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION; @@ -116,8 +100,6 @@ function sdkInitEnvironmentToJSON(object) { switch (object) { case SdkInitEnvironment.SDK_INIT_ENVIRONMENT_DEVELOPMENT: return "SDK_INIT_ENVIRONMENT_DEVELOPMENT"; - case SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING: - return "SDK_INIT_ENVIRONMENT_STAGING"; case SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION: return "SDK_INIT_ENVIRONMENT_PRODUCTION"; case SdkInitEnvironment.UNRECOGNIZED: diff --git a/sdk/shared/proto-ts/src/convenience/model_types_convenience.ts b/sdk/shared/proto-ts/src/convenience/model_types_convenience.ts index 045d9a36a5..6f7f451175 100644 --- a/sdk/shared/proto-ts/src/convenience/model_types_convenience.ts +++ b/sdk/shared/proto-ts/src/convenience/model_types_convenience.ts @@ -130,8 +130,6 @@ export const sDKEnvironmentWireString = (e: SDKEnvironment): string => { return 'unspecified'; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return 'development'; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return 'staging'; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return 'production'; default: @@ -145,8 +143,6 @@ export const sDKEnvironmentFromWireString = (s: string): SDKEnvironment | undefi return SDKEnvironment.SDK_ENVIRONMENT_UNSPECIFIED; case 'development': return SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - case 'staging': - return SDKEnvironment.SDK_ENVIRONMENT_STAGING; case 'production': return SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; default: diff --git a/sdk/shared/proto-ts/src/model_types.ts b/sdk/shared/proto-ts/src/model_types.ts index 20df35f5be..202b3a8e4a 100644 --- a/sdk/shared/proto-ts/src/model_types.ts +++ b/sdk/shared/proto-ts/src/model_types.ts @@ -498,19 +498,14 @@ export function modelCategoryToJSON(object: ModelCategory): string { /** * --------------------------------------------------------------------------- - * SDK environment. Sources pre-IDL: - * Swift SDKEnvironment.swift:5 (development, staging, production) - * Kotlin RunAnywhere.kt:47 (DEVELOPMENT, STAGING, PRODUCTION, cEnvironment) - * Kotlin SDKLogger.kt:159 (DEVELOPMENT, STAGING, PRODUCTION) ← duplicate - * Dart sdk_environment.dart:5 (development, staging, production) - * RN enums.ts:11 (Development, Staging, Production) - * Web enums.ts:9 (Development, Staging, Production) + * SDK environment — product surface is development + production only. + * Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values + * never shift PRODUCTION=3. * --------------------------------------------------------------------------- */ export enum SDKEnvironment { SDK_ENVIRONMENT_UNSPECIFIED = 0, SDK_ENVIRONMENT_DEVELOPMENT = 1, - SDK_ENVIRONMENT_STAGING = 2, SDK_ENVIRONMENT_PRODUCTION = 3, UNRECOGNIZED = -1, } @@ -523,9 +518,6 @@ export function sDKEnvironmentFromJSON(object: any): SDKEnvironment { case 1: case "SDK_ENVIRONMENT_DEVELOPMENT": return SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT; - case 2: - case "SDK_ENVIRONMENT_STAGING": - return SDKEnvironment.SDK_ENVIRONMENT_STAGING; case 3: case "SDK_ENVIRONMENT_PRODUCTION": return SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION; @@ -542,8 +534,6 @@ export function sDKEnvironmentToJSON(object: SDKEnvironment): string { return "SDK_ENVIRONMENT_UNSPECIFIED"; case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: return "SDK_ENVIRONMENT_DEVELOPMENT"; - case SDKEnvironment.SDK_ENVIRONMENT_STAGING: - return "SDK_ENVIRONMENT_STAGING"; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return "SDK_ENVIRONMENT_PRODUCTION"; case SDKEnvironment.UNRECOGNIZED: diff --git a/sdk/shared/proto-ts/src/sdk_init.ts b/sdk/shared/proto-ts/src/sdk_init.ts index c181578945..d363f0c28a 100644 --- a/sdk/shared/proto-ts/src/sdk_init.ts +++ b/sdk/shared/proto-ts/src/sdk_init.ts @@ -69,26 +69,13 @@ export function sdkInitPhaseToJSON(object: SdkInitPhase): string { * --------------------------------------------------------------------------- * Environment values — must match RAC_ENV_* in * sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h - * (development=0, staging=1, production=2). Numeric values are part of the - * wire format; do not reorder. - * - * The prior attempt to - * add SDK_INIT_ENVIRONMENT_UNSPECIFIED=0 and bump the tristate to 1/2/3 broke - * Swift iOS at runtime — the shipped librac_commons.a in - * sdk/runanywhere-swift/Binaries/RACommons.xcframework was compiled with the - * original 0/1/2 layout, so Swift sending the regenerated enum value 1 - * (DEVELOPMENT) was decoded as STAGING by the old C++ side, which then failed - * validation with RAC_ERROR_INVALID_ARGUMENT ("API key required"). The other - * SDKs (Kotlin / Flutter / RN / Web) were never regenerated for the bumped - * layout either, so reverting to the original 0/1/2 wire-format restores - * cross-SDK consistency without requiring a coordinated xcframework rebuild. - * Re-introducing UNSPECIFIED=0 must be paired with a synchronized rebuild of - * every prebuilt commons binary AND regeneration of all five SDK bindings. + * (development=0, production=2). Numeric values are part of the wire format; + * do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is + * reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). * --------------------------------------------------------------------------- */ export enum SdkInitEnvironment { SDK_INIT_ENVIRONMENT_DEVELOPMENT = 0, - SDK_INIT_ENVIRONMENT_STAGING = 1, SDK_INIT_ENVIRONMENT_PRODUCTION = 2, UNRECOGNIZED = -1, } @@ -98,9 +85,6 @@ export function sdkInitEnvironmentFromJSON(object: any): SdkInitEnvironment { case 0: case "SDK_INIT_ENVIRONMENT_DEVELOPMENT": return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_DEVELOPMENT; - case 1: - case "SDK_INIT_ENVIRONMENT_STAGING": - return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING; case 2: case "SDK_INIT_ENVIRONMENT_PRODUCTION": return SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION; @@ -115,8 +99,6 @@ export function sdkInitEnvironmentToJSON(object: SdkInitEnvironment): string { switch (object) { case SdkInitEnvironment.SDK_INIT_ENVIRONMENT_DEVELOPMENT: return "SDK_INIT_ENVIRONMENT_DEVELOPMENT"; - case SdkInitEnvironment.SDK_INIT_ENVIRONMENT_STAGING: - return "SDK_INIT_ENVIRONMENT_STAGING"; case SdkInitEnvironment.SDK_INIT_ENVIRONMENT_PRODUCTION: return "SDK_INIT_ENVIRONMENT_PRODUCTION"; case SdkInitEnvironment.UNRECOGNIZED: From 7ebe4d99c563608de508b634c06807d3a78da301 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 18:43:52 -0700 Subject: [PATCH 41/44] ci: path-filter OSS keyless telemetry on relevant PRs Lets PR #569 prove the gate with the branch's rcli (main lacks telemetry blast). Schedule/dispatch still apply once the workflow is on the default branch. Co-authored-by: Cursor --- .github/workflows/oss-keyless-telemetry.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/oss-keyless-telemetry.yml b/.github/workflows/oss-keyless-telemetry.yml index 91eda1d7b1..ebbbd61eb9 100644 --- a/.github/workflows/oss-keyless-telemetry.yml +++ b/.github/workflows/oss-keyless-telemetry.yml @@ -9,13 +9,21 @@ name: OSS keyless telemetry on: + pull_request: + paths: + - ".github/workflows/oss-keyless-telemetry.yml" + - "scripts/ci/oss_keyless_telemetry_blast.sh" + - "sdk/runanywhere-cli/**" + - "sdk/runanywhere-commons/src/infrastructure/network/**" + - "sdk/runanywhere-commons/src/lifecycle/**" + - "sdk/runanywhere-commons/src/core/sdk_state.cpp" schedule: - # Daily 08:00 UTC — not on PRs/pushes (avoids burning macos runners every merge). + # Daily 08:00 UTC — light cadence for Staging drift. - cron: "0 8 * * *" workflow_dispatch: {} concurrency: - group: ${{ github.workflow }}-scheduled + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: From 4eecce3b6a48620477ff65397448a0f5a8b5277c Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 19:06:27 -0700 Subject: [PATCH 42/44] changes --- sdk/runanywhere-cli/CMakeLists.txt | 5 + .../src/commands/cmd_telemetry.cpp | 54 +++++++--- sdk/runanywhere-cli/src/device_info.cpp | 98 ++++++++++++++++++- 3 files changed, 144 insertions(+), 13 deletions(-) diff --git a/sdk/runanywhere-cli/CMakeLists.txt b/sdk/runanywhere-cli/CMakeLists.txt index 2fb1a107fe..3ff5a517f7 100644 --- a/sdk/runanywhere-cli/CMakeLists.txt +++ b/sdk/runanywhere-cli/CMakeLists.txt @@ -93,6 +93,11 @@ target_include_directories(rcli_core PUBLIC target_link_libraries(rcli_core PUBLIC rac_commons) target_compile_features(rcli_core PUBLIC cxx_std_20) +# macOS battery / memory sampling in device_info.cpp (IOKit + CoreFoundation). +if(APPLE) + target_link_libraries(rcli_core PUBLIC "-framework IOKit" "-framework CoreFoundation") +endif() + # Version string: single source of truth is sdk/runanywhere-commons/VERSION, # already read into PROJECT_VERSION by the root CMakeLists. target_compile_definitions(rcli_core PUBLIC diff --git a/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp b/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp index a5f14bc7c7..356b8ef61f 100644 --- a/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp +++ b/sdk/runanywhere-cli/src/commands/cmd_telemetry.cpp @@ -49,21 +49,25 @@ namespace { struct ModalitySpec { const char* name; const char* default_event_type; + // Synthetic probe identity — blast/emit always stamp these so quality + // gates never see null model_id/framework on the control-plane path. + const char* probe_model_id; + const char* probe_framework; }; constexpr ModalitySpec kModalities[] = { - {"llm", "llm.generation.completed"}, - {"stt", "stt.transcription.completed"}, - {"tts", "tts.synthesis.completed"}, - {"vlm", "vlm.process.completed"}, - {"rag", "rag.query.completed"}, - {"imagegen", "imagegen.generate.completed"}, - {"embeddings", "embeddings.embed.completed"}, - {"vad", "vad.stopped"}, - {"voice", "voice.turn.metrics"}, - {"lora", "lora.attach.completed"}, - {"model", "model.download.completed"}, - {"system", "sdk.init.completed"}, + {"llm", "llm.generation.completed", "probe-llm-qwen2.5-0.5b", "llamacpp"}, + {"stt", "stt.transcription.completed", "probe-stt-whisper-tiny", "sherpa"}, + {"tts", "tts.synthesis.completed", "probe-tts-piper", "sherpa"}, + {"vlm", "vlm.process.completed", "probe-vlm-llava-1.5", "llamacpp"}, + {"rag", "rag.query.completed", "probe-rag-minilm", "llamacpp"}, + {"imagegen", "imagegen.generate.completed", "probe-imagegen-sd-turbo", "coreml"}, + {"embeddings", "embeddings.embed.completed", "probe-embed-minilm", "onnx"}, + {"vad", "vad.stopped", "probe-vad-silero", "onnx"}, + {"voice", "voice.turn.metrics", "probe-voice-pipeline", "llamacpp"}, + {"lora", "lora.attach.completed", "probe-lora-base", "llamacpp"}, + {"model", "model.download.completed", "probe-model-qwen2.5-0.5b", "llamacpp"}, + {"system", "sdk.init.completed", "probe-sdk-system", "llamacpp"}, }; const ModalitySpec* find_modality(const std::string& name) { @@ -179,6 +183,9 @@ void track_events(rac_telemetry_manager_t* manager, const ModalitySpec& spec, payload.event_type = event_type.c_str(); payload.modality = spec.name; payload.session_id = session_id.c_str(); + payload.model_id = spec.probe_model_id; + payload.model_name = spec.probe_model_id; + payload.framework = spec.probe_framework; const int64_t now_ms = rac_get_current_time_ms(); payload.timestamp_ms = now_ms; payload.created_at_ms = now_ms; @@ -196,8 +203,31 @@ void track_events(rac_telemetry_manager_t* manager, const ModalitySpec& spec, payload.total_tokens = (metrics.input_tokens > 0 ? metrics.input_tokens : 0) + metrics.output_tokens; } + // LLM/VLM timing probes: derive coherent values from processing_ms + + // output tokens so quality SQL can assert non-null TPS/TTFT/etc. + const bool token_modality = + std::string(spec.name) == "llm" || std::string(spec.name) == "vlm"; + if (token_modality && metrics.processing_ms > 0 && metrics.output_tokens > 0) { + const double prompt_ms = metrics.processing_ms * 0.30; + const double gen_ms = metrics.processing_ms - prompt_ms; + payload.prompt_eval_time_ms = prompt_ms; + payload.generation_time_ms = gen_ms; + payload.time_to_first_token_ms = prompt_ms; + payload.tokens_per_second = + static_cast(metrics.output_tokens) / (gen_ms / 1000.0); + payload.context_length = 4096; + payload.temperature = 0.7; + payload.max_tokens = metrics.output_tokens; + } if (metrics.audio_duration_ms >= 0) { payload.audio_duration_ms = metrics.audio_duration_ms; + } else if (std::string(spec.name) == "stt" && metrics.processing_ms > 0) { + // Default STT audio length when not specified (RTF-friendly probe). + payload.audio_duration_ms = metrics.processing_ms * 10.0; + payload.real_time_factor = 0.1; + payload.word_count = 12; + payload.confidence = 0.91; + payload.language = "en"; } rac_telemetry_manager_track(manager, &payload); } diff --git a/sdk/runanywhere-cli/src/device_info.cpp b/sdk/runanywhere-cli/src/device_info.cpp index 3c7253dbb4..a827681d26 100644 --- a/sdk/runanywhere-cli/src/device_info.cpp +++ b/sdk/runanywhere-cli/src/device_info.cpp @@ -18,6 +18,10 @@ #if defined(_WIN32) #include #elif defined(__APPLE__) +#include +#include +#include +#include #include #include #include @@ -299,6 +303,96 @@ int64_t sysctl_i64(const char *key) { return value; } +int64_t macos_available_memory_bytes() { + mach_port_t host = mach_host_self(); + vm_statistics64_data_t stats = {}; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + if (host_statistics64(host, HOST_VM_INFO64, + reinterpret_cast(&stats), + &count) != KERN_SUCCESS) { + return 0; + } + const int64_t page_size = static_cast(sysctl_i64("hw.pagesize")); + if (page_size <= 0) { + return 0; + } + // Free + speculative pages ≈ "available" for telemetry (not wired/compressed). + const int64_t free_pages = + static_cast(stats.free_count) + + static_cast(stats.purgeable_count); + return free_pages * page_size; +} + +void macos_sample_battery(DeviceInfoState &info) { + // Desktop Macs (Studio/Mini/iMac) have no battery → leave level=-1 (null). + // Laptops expose IOPowerSources; sample real capacity rather than inventing 0. + CFTypeRef blob = IOPSCopyPowerSourcesInfo(); + if (blob == nullptr) { + return; + } + CFArrayRef list = IOPSCopyPowerSourcesList(blob); + if (list == nullptr) { + CFRelease(blob); + return; + } + + const CFIndex count = CFArrayGetCount(list); + for (CFIndex i = 0; i < count; ++i) { + CFTypeRef ps = CFArrayGetValueAtIndex(list, i); + CFDictionaryRef desc = IOPSGetPowerSourceDescription(blob, ps); + if (desc == nullptr) { + continue; + } + auto number_for = [&](CFStringRef key) -> double { + const auto *num = + static_cast(CFDictionaryGetValue(desc, key)); + if (num == nullptr) { + return -1.0; + } + double value = -1.0; + return CFNumberGetValue(num, kCFNumberDoubleType, &value) ? value : -1.0; + }; + + const double current = number_for(CFSTR(kIOPSCurrentCapacityKey)); + const double max_cap = number_for(CFSTR(kIOPSMaxCapacityKey)); + if (current < 0.0) { + continue; + } + // Capacity is usually already a percent (0–100); normalize to 0–1. + double level = current; + if (max_cap > 0.0 && max_cap != 100.0) { + level = (current / max_cap) * 100.0; + } + if (level > 1.0) { + level /= 100.0; + } + if (level < 0.0 || level > 1.0) { + continue; + } + + info.battery_level = level; + info.form_factor = "laptop"; + + const auto *state = static_cast( + CFDictionaryGetValue(desc, CFSTR(kIOPSPowerSourceStateKey))); + const auto *charging = static_cast( + CFDictionaryGetValue(desc, CFSTR(kIOPSIsChargingKey))); + if (charging != nullptr && CFBooleanGetValue(charging)) { + info.battery_state = level >= 0.999 ? "full" : "charging"; + } else if (state != nullptr && + CFStringCompare(state, CFSTR(kIOPSACPowerValue), 0) == + kCFCompareEqualTo) { + info.battery_state = level >= 0.999 ? "full" : "charging"; + } else { + info.battery_state = "unplugged"; + } + break; + } + + CFRelease(list); + CFRelease(blob); +} + void collect_device_info(DeviceInfoState &info) { info.platform = "macos"; @@ -323,7 +417,7 @@ void collect_device_info(DeviceInfoState &info) { } info.total_memory = sysctl_i64("hw.memsize"); - info.available_memory = 0; + info.available_memory = macos_available_memory_bytes(); const int64_t ncpu = sysctl_i64("hw.ncpu"); info.core_count = ncpu > 0 ? static_cast(ncpu) : 1; @@ -349,6 +443,8 @@ void collect_device_info(DeviceInfoState &info) { #else info.gpu_family = "unknown"; #endif + + macos_sample_battery(info); } #else // _WIN32 From a727bbc3ced38e4366d6a8ad1bb06b540f7d35e0 Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 20:00:17 -0700 Subject: [PATCH 43/44] fix(ci): repair staging-removal breakages that reddened PR checks - Regenerate Wire/Dart bindings so IDL drift check matches CI codegen - Remove orphaned Flutter switch break and undefined isStaging - Repair web SDKLogger switch left half-deleted by staging cleanup - Export rac_env_auth_expected + rac_dev_config_get_staging_base_url - Use C++20 for keyless live test; persist-credentials false on OSS workflow - TTS.synthesizeStreamAuto checks lifecycle capability flag Co-authored-by: Cursor --- .github/workflows/oss-keyless-telemetry.yml | 2 ++ .../exports/RACommons.exports | 2 ++ sdk/runanywhere-commons/tests/CMakeLists.txt | 2 +- .../runanywhere/lib/generated/logging.pb.dart | 2 +- .../runanywhere/lib/native/dart_bridge.dart | 1 - .../runanywhere/lib/public/runanywhere.dart | 33 ++++++++----------- .../proto/v1/LoggingConfiguration.kt | 2 +- .../ai/runanywhere/proto/v1/SDKEnvironment.kt | 6 ++-- .../proto/v1/SdkInitEnvironment.kt | 11 +++++-- .../packages/core/src/Foundation/SDKLogger.ts | 7 +--- .../src/Public/Extensions/RunAnywhere+TTS.ts | 4 +-- 11 files changed, 35 insertions(+), 37 deletions(-) diff --git a/.github/workflows/oss-keyless-telemetry.yml b/.github/workflows/oss-keyless-telemetry.yml index ebbbd61eb9..c90c8a365c 100644 --- a/.github/workflows/oss-keyless-telemetry.yml +++ b/.github/workflows/oss-keyless-telemetry.yml @@ -36,6 +36,8 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Install ninja + protobuf run: brew install ninja protobuf diff --git a/sdk/runanywhere-commons/exports/RACommons.exports b/sdk/runanywhere-commons/exports/RACommons.exports index e8b1404b18..7903e6cb87 100644 --- a/sdk/runanywhere-commons/exports/RACommons.exports +++ b/sdk/runanywhere-commons/exports/RACommons.exports @@ -481,6 +481,7 @@ _rac_sdk_set_client_info # SDKEnvironment helpers + URL / API key validators called from # Public/Configuration/SDKEnvironment.swift and CppBridge+Environment.swift. +_rac_env_auth_expected _rac_env_default_log_level _rac_env_description _rac_env_is_production @@ -497,6 +498,7 @@ _rac_validation_error_message # Dev config (staging base URL + shared usability checks) — reached # through CppBridge+Environment.swift. +_rac_dev_config_get_staging_base_url _rac_dev_config_is_usable_credential _rac_dev_config_is_usable_http_url diff --git a/sdk/runanywhere-commons/tests/CMakeLists.txt b/sdk/runanywhere-commons/tests/CMakeLists.txt index a00546d030..bf2969d953 100644 --- a/sdk/runanywhere-commons/tests/CMakeLists.txt +++ b/sdk/runanywhere-commons/tests/CMakeLists.txt @@ -1679,5 +1679,5 @@ if(CURL_FOUND) target_include_directories(test_development_keyless_live PRIVATE ${CMAKE_SOURCE_DIR}/include) target_link_libraries(test_development_keyless_live PRIVATE rac_commons CURL::libcurl Threads::Threads) rac_link_archive_deps(test_development_keyless_live) - target_compile_features(test_development_keyless_live PRIVATE cxx_std_17) + target_compile_features(test_development_keyless_live PRIVATE cxx_std_20) endif() diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart index 5b6b018785..dd2824fe97 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/generated/logging.pb.dart @@ -23,7 +23,7 @@ export 'logging.pbenum.dart'; /// --------------------------------------------------------------------------- /// SDK logging configuration. Per-environment presets -/// (development/production) stay in each SDK as factory helpers. +/// (development/staging/production) stay in each SDK as factory helpers. /// --------------------------------------------------------------------------- class LoggingConfiguration extends $pb.GeneratedMessage { factory LoggingConfiguration({ diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart index dfe1e2c256..59c6acb72f 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart @@ -563,7 +563,6 @@ class DartBridge { static void _configureLogging(SDKEnvironment environment) { int logLevel; switch (environment) { - break; case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: logLevel = RacLogLevel.warning; break; diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index 570bc55c08..b2b1a4674c 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -507,38 +507,31 @@ abstract final class RunAnywhere { environment: environment, ); } else { - // Keyless staging is valid: commons overrides the base URL with the - // baked staging backend and requests go out unauthenticated - // (PUBLIC-org ingestion). Production stays strict. - if (!isStaging && (apiKey == null || apiKey.isEmpty)) { + // Production (and any non-development env): API key + HTTPS base URL. + final trimmedKey = (apiKey ?? '').trim(); + final trimmedUrl = (baseURL ?? '').trim(); + if (trimmedKey.isEmpty) { throw SDKException.validationFailed( 'API key is required for ${environment.description} mode', fieldPath: 'SDKInitParams.apiKey', ); } - if (!isStaging && (baseURL == null || baseURL.isEmpty)) { + if (trimmedUrl.isEmpty) { throw SDKException.validationFailed( 'Base URL is required for ${environment.description} mode', fieldPath: 'SDKInitParams.baseURL', ); } - final Uri uri; - if (baseURL == null || baseURL.isEmpty) { - // Staging placeholder — replaced by the baked staging URL in commons. - uri = Uri.parse('https://staging.runanywhere.local'); - } else { - final parsed = Uri.tryParse(baseURL); - if (parsed == null) { - throw SDKException.validationFailed( - 'Invalid base URL: $baseURL', - fieldPath: 'SDKInitParams.baseURL', - ); - } - uri = parsed; + final parsed = Uri.tryParse(trimmedUrl); + if (parsed == null) { + throw SDKException.validationFailed( + 'Invalid base URL: $trimmedUrl', + fieldPath: 'SDKInitParams.baseURL', + ); } params = SDKInitParams( - apiKey: apiKey ?? '', - baseURL: uri, + apiKey: trimmedKey, + baseURL: parsed, environment: environment, ); } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt index 982be2c657..8eeb7c77f5 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/LoggingConfiguration.kt @@ -31,7 +31,7 @@ import okio.ByteString /** * --------------------------------------------------------------------------- * SDK logging configuration. Per-environment presets - * (development/production) stay in each SDK as factory helpers. + * (development/staging/production) stay in each SDK as factory helpers. * --------------------------------------------------------------------------- */ public class LoggingConfiguration( diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt index 5ecc903804..c718ccac85 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SDKEnvironment.kt @@ -17,8 +17,11 @@ import kotlin.Int import kotlin.Suppress /** + * --------------------------------------------------------------------------- * SDK environment — product surface is development + production only. + * Number 2 was formerly SDK_ENVIRONMENT_STAGING; reserved so wire values * never shift PRODUCTION=3. + * --------------------------------------------------------------------------- */ public enum class SDKEnvironment( override val `value`: Int, @@ -45,8 +48,7 @@ public enum class SDKEnvironment( public fun fromValue(`value`: Int): SDKEnvironment? = when (`value`) { 0 -> SDK_ENVIRONMENT_UNSPECIFIED 1 -> SDK_ENVIRONMENT_DEVELOPMENT - // Former staging (=2) → treat as production at the Kotlin boundary. - 2, 3 -> SDK_ENVIRONMENT_PRODUCTION + 3 -> SDK_ENVIRONMENT_PRODUCTION else -> null } } diff --git a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt index 9ce71ce037..11609b521d 100644 --- a/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt +++ b/sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/SdkInitEnvironment.kt @@ -17,7 +17,13 @@ import kotlin.Int import kotlin.Suppress /** - * Environment values — must match RAC_ENV_* (development=0, production=2). + * --------------------------------------------------------------------------- + * Environment values — must match RAC_ENV_* in + * sdk/runanywhere-commons/include/rac/infrastructure/network/rac_environment.h + * (development=0, production=2). Numeric values are part of the wire format; + * do not reorder. Number 1 was formerly SDK_INIT_ENVIRONMENT_STAGING and is + * reserved so PRODUCTION stays at 2 (shipped commons / xcframework layout). + * --------------------------------------------------------------------------- */ public enum class SdkInitEnvironment( override val `value`: Int, @@ -39,8 +45,7 @@ public enum class SdkInitEnvironment( @JvmStatic public fun fromValue(`value`: Int): SdkInitEnvironment? = when (`value`) { 0 -> SDK_INIT_ENVIRONMENT_DEVELOPMENT - // Former staging (=1) → production. - 1, 2 -> SDK_INIT_ENVIRONMENT_PRODUCTION + 2 -> SDK_INIT_ENVIRONMENT_PRODUCTION else -> null } } diff --git a/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts b/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts index 8ce81bf87e..6d2606868f 100644 --- a/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts +++ b/sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts @@ -33,12 +33,6 @@ export function loggingConfigurationForEnvironment( environment: SDKEnvironment, ): LoggingConfiguration { switch (environment) { - enableLocalLogging: true, - minLogLevel: LogLevel.LOG_LEVEL_INFO, - includeSourceLocation: false, - includeDeviceMetadata: true, - enableRemoteLogging: false, - }); case SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION: return LoggingConfigurationProto.fromPartial({ enableLocalLogging: false, @@ -47,6 +41,7 @@ export function loggingConfigurationForEnvironment( includeDeviceMetadata: true, enableRemoteLogging: false, }); + case SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT: default: return LoggingConfigurationProto.fromPartial({ enableLocalLogging: true, diff --git a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts index 1bdc1ac01b..f3c7923b69 100644 --- a/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts +++ b/sdk/runanywhere-web/packages/core/src/Public/Extensions/RunAnywhere+TTS.ts @@ -203,10 +203,10 @@ export const TTS = { options?: Partial, ): AsyncIterable { const adapter = TTSProtoAdapter.tryDefault(); - if (!adapter || !adapter.supportsProtoTTS()) { + if (!adapter || !adapter.supportsLifecycleProtoTTS()) { throw SDKException.backendNotAvailable( 'TTS.synthesizeStreamAuto', - 'No Web WASM backend with rac_tts_*_proto exports is registered.', + 'No Web WASM backend with rac_tts_*_proto lifecycle exports is registered.', ); } return adapter.synthesizeLifecycleStream(text, defaultTTSOptions(options)); From 6c4b5af914da4c50e0e6c389ec6fd29ebca9e03c Mon Sep 17 00:00:00 2001 From: Shubham Malhotra Date: Wed, 22 Jul 2026 20:18:51 -0700 Subject: [PATCH 44/44] minor changes --- .github/workflows/release.yml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2403134456..7ade2680ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,8 +42,9 @@ env: # — lets environment=development run keyless with no explicit URL (see # rac_dev_config_get_staging_base_url). Kept in a CI secret (never in the public # source tree); the commons CMake substitutes it into the generated - # development_config.cpp when present (else a placeholder stub). No credentials, - # project refs, or tokens are ever embedded. + # development_config.cpp. validate fails closed if the secret is missing / + # unusable so release artifacts never ship the placeholder stub. No + # credentials, project refs, or tokens are ever embedded. STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} jobs: @@ -57,6 +58,29 @@ jobs: version: ${{ steps.parse.outputs.version }} steps: - uses: actions/checkout@v7 + - name: Require usable STAGING_BASE_URL for release bake + env: + STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} + run: | + set -euo pipefail + # Fail closed: empty/placeholder secrets bake YOUR_STAGING_BASE_URL into + # development_config.cpp, which rac_dev_config_is_usable_http_url rejects — + # released binaries then cannot keyless-resolve a development backend. + if [[ -z "${STAGING_BASE_URL:-}" ]]; then + echo "::error::Repository secret STAGING_BASE_URL is unset or empty. Set it to the public staging backend origin (https://…) before running the release train." + exit 1 + fi + case "${STAGING_BASE_URL}" in + YOUR_STAGING_BASE_URL|http://YOUR_*|https://YOUR_*|placeholder*|PLACEHOLDER*) + echo "::error::STAGING_BASE_URL looks like a placeholder ('${STAGING_BASE_URL}'). Set the real public staging backend origin." + exit 1 + ;; + esac + if [[ ! "${STAGING_BASE_URL}" =~ ^https?://[^[:space:]]+$ ]]; then + echo "::error::STAGING_BASE_URL must be an http(s) origin without whitespace (got '${STAGING_BASE_URL}')." + exit 1 + fi + echo "STAGING_BASE_URL is set and looks usable for release bake." - name: Parse and validate version id: parse run: |